mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-10 13:43:54 +00:00
Improve embeds usability.
This commit is contained in:
@@ -7888,6 +7888,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_contact_send_message" = "Message";
|
||||
|
||||
"lng_iv_open_in_browser" = "Open in Browser";
|
||||
"lng_iv_click_to_view" = "Click to View";
|
||||
"lng_iv_share" = "Share";
|
||||
"lng_iv_join_channel" = "Join";
|
||||
"lng_iv_window_title" = "Instant View";
|
||||
|
||||
@@ -166,6 +166,10 @@ MarkdownEmbedPost {
|
||||
MarkdownPlaceholder {
|
||||
padding: margins;
|
||||
minHeight: pixels;
|
||||
border: pixels;
|
||||
radius: pixels;
|
||||
spinnerWidth: pixels;
|
||||
spinnerSize: pixels;
|
||||
captionSkip: pixels;
|
||||
labelStyle: TextStyle;
|
||||
}
|
||||
@@ -417,8 +421,12 @@ defaultMarkdownEmbedPost: MarkdownEmbedPost {
|
||||
captionSkip: 12px;
|
||||
}
|
||||
defaultMarkdownPlaceholder: MarkdownPlaceholder {
|
||||
padding: margins(16px, 12px, 16px, 12px);
|
||||
padding: margins(16px, 24px, 16px, 24px);
|
||||
minHeight: 48px;
|
||||
border: 4px;
|
||||
radius: 16px;
|
||||
spinnerWidth: 4px;
|
||||
spinnerSize: 24px;
|
||||
captionSkip: 14px;
|
||||
labelStyle: TextStyle(defaultMarkdownBodyStyle) {
|
||||
font: font(16px semibold);
|
||||
|
||||
@@ -16,10 +16,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/dynamic_image.h"
|
||||
|
||||
#include "styles/style_iv.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace Iv::Markdown {
|
||||
@@ -109,6 +111,54 @@ void RestoreRelatedArticleThumbnailStates(
|
||||
}
|
||||
}
|
||||
|
||||
void CollectPlaceholderIds(
|
||||
const std::vector<LaidOutBlock> &blocks,
|
||||
std::unordered_set<uint64> *result) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
for (const auto &block : blocks) {
|
||||
if (block.placeholderId) {
|
||||
result->emplace(block.placeholderId.value);
|
||||
}
|
||||
CollectPlaceholderIds(block.children, result);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] LaidOutBlock *FindPlaceholderBlock(
|
||||
std::vector<LaidOutBlock> *blocks,
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (!blocks || !id) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto &block : *blocks) {
|
||||
if (block.placeholderId.value == id.value) {
|
||||
return █
|
||||
}
|
||||
if (const auto child = FindPlaceholderBlock(&block.children, id)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] const LaidOutBlock *FindPlaceholderBlock(
|
||||
const std::vector<LaidOutBlock> &blocks,
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (!id) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const auto &block : blocks) {
|
||||
if (block.placeholderId.value == id.value) {
|
||||
return █
|
||||
}
|
||||
if (const auto child = FindPlaceholderBlock(block.children, id)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] PendingHighlightKey PendingHighlightKeyForBlock(
|
||||
const LaidOutBlock &block) {
|
||||
return {
|
||||
@@ -310,6 +360,12 @@ void RebuildVisibleSegmentLookup(
|
||||
}
|
||||
} else {
|
||||
applyActivation(segment.block->activation);
|
||||
if (result.mediaActivation.kind == MediaActivationKind::Embed
|
||||
&& segment.block->placeholderRuntime) {
|
||||
result.state.link = segment.block->placeholderRuntime->clickHandler;
|
||||
result.placeholderLocalPoint = point
|
||||
- segment.block->mediaRect.topLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
result.direct = true;
|
||||
@@ -377,6 +433,18 @@ void ClearColorizedFormulaImages(std::vector<LaidOutBlock> *blocks) {
|
||||
|
||||
} // namespace
|
||||
|
||||
PlaceholderBlockRuntime::PlaceholderBlockRuntime(Fn<void()> repaint)
|
||||
: clickHandler(std::make_shared<LambdaClickHandler>([] {
|
||||
}))
|
||||
, loadingAnimation(
|
||||
[repaint = std::move(repaint)] {
|
||||
if (repaint) {
|
||||
repaint();
|
||||
}
|
||||
},
|
||||
st::defaultInfiniteRadialAnimation) {
|
||||
}
|
||||
|
||||
class MarkdownArticle::Impl final : public CodeBlockSyntaxHighlightTracker {
|
||||
public:
|
||||
explicit Impl(std::shared_ptr<MathRenderer> renderer);
|
||||
@@ -446,6 +514,12 @@ public:
|
||||
|
||||
[[nodiscard]] MediaBlockHost *mediaBlockHost() const;
|
||||
|
||||
void setPlaceholderLoading(PreparedPlaceholderBlockId id);
|
||||
void clearPlaceholderLoading(PreparedPlaceholderBlockId id);
|
||||
void clearAllPlaceholderLoading();
|
||||
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
|
||||
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
|
||||
|
||||
void invalidateLayout();
|
||||
|
||||
private:
|
||||
@@ -459,6 +533,15 @@ private:
|
||||
|
||||
void refreshMediaBlockHosts();
|
||||
|
||||
void clearPlaceholderRuntimes();
|
||||
|
||||
[[nodiscard]] std::shared_ptr<PlaceholderBlockRuntime>
|
||||
getOrCreatePlaceholderRuntime(PreparedPlaceholderBlockId id);
|
||||
|
||||
void prunePlaceholderRuntimes();
|
||||
|
||||
void requestPlaceholderRepaint(PreparedPlaceholderBlockId id);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<MediaBlock> getOrCreateMediaBlock(
|
||||
const PreparedBlock &prepared);
|
||||
|
||||
@@ -486,6 +569,10 @@ private:
|
||||
|
||||
void resetFormulaRasterCache();
|
||||
|
||||
void setPlaceholderLoadingValue(
|
||||
PreparedPlaceholderBlockId id,
|
||||
bool loading);
|
||||
|
||||
void relayout(int width);
|
||||
|
||||
mutable MarkdownArticleContent _content;
|
||||
@@ -499,6 +586,8 @@ private:
|
||||
int _height = 0;
|
||||
std::vector<LaidOutBlock> _blocks;
|
||||
std::unordered_map<uint64, std::shared_ptr<MediaBlock>> _mediaBlocks;
|
||||
std::unordered_map<uint64, std::shared_ptr<PlaceholderBlockRuntime>>
|
||||
_placeholderRuntimes;
|
||||
std::unordered_map<
|
||||
uint64,
|
||||
RelatedArticleThumbnailState> _relatedArticleThumbnails;
|
||||
@@ -544,6 +633,7 @@ void MarkdownArticle::Impl::setTextRepaintCallbacks(
|
||||
|
||||
void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) {
|
||||
clearMediaBlocks();
|
||||
clearPlaceholderRuntimes();
|
||||
_relatedArticleThumbnails.clear();
|
||||
_content = std::move(content);
|
||||
ClearInlineFormulaObjectCache(_inlineFormulaObjects);
|
||||
@@ -761,6 +851,79 @@ MediaBlockHost *MarkdownArticle::Impl::mediaBlockHost() const {
|
||||
return _mediaBlockHost;
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::setPlaceholderLoading(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
setPlaceholderLoadingValue(id, true);
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::clearPlaceholderLoading(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
setPlaceholderLoadingValue(id, false);
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::clearAllPlaceholderLoading() {
|
||||
auto repaintIds = std::vector<PreparedPlaceholderBlockId>();
|
||||
repaintIds.reserve(_placeholderRuntimes.size());
|
||||
for (const auto &[value, runtime] : _placeholderRuntimes) {
|
||||
if (!runtime || !runtime->loading) {
|
||||
continue;
|
||||
}
|
||||
runtime->loading = false;
|
||||
runtime->loadingAnimation.stop(anim::type::instant);
|
||||
repaintIds.push_back({ .value = value });
|
||||
}
|
||||
for (const auto id : repaintIds) {
|
||||
requestPlaceholderRepaint(id);
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::addPlaceholderRipple(
|
||||
PreparedPlaceholderBlockId id,
|
||||
QPoint point) {
|
||||
const auto block = FindPlaceholderBlock(&_blocks, id);
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
auto runtime = block->placeholderRuntime
|
||||
? block->placeholderRuntime
|
||||
: getOrCreatePlaceholderRuntime(id);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
block->placeholderRuntime = runtime;
|
||||
const auto size = block->mediaRect.size();
|
||||
if (!runtime->ripple || runtime->rippleSize != size) {
|
||||
runtime->ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
Ui::RippleAnimation::RoundRectMask(
|
||||
size,
|
||||
st::defaultMarkdown.placeholder.radius),
|
||||
[=] {
|
||||
requestPlaceholderRepaint(id);
|
||||
});
|
||||
runtime->rippleSize = size;
|
||||
}
|
||||
point.setX(std::clamp(point.x(), 0, std::max(size.width() - 1, 0)));
|
||||
point.setY(std::clamp(point.y(), 0, std::max(size.height() - 1, 0)));
|
||||
runtime->ripple->add(point);
|
||||
requestPlaceholderRepaint(id);
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::stopPlaceholderRipple(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const auto i = _placeholderRuntimes.find(id.value);
|
||||
if (i == end(_placeholderRuntimes)
|
||||
|| !i->second
|
||||
|| !i->second->ripple) {
|
||||
return;
|
||||
}
|
||||
i->second->ripple->lastStop();
|
||||
requestPlaceholderRepaint(id);
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::invalidateLayout() {
|
||||
_width = -1;
|
||||
_height = 0;
|
||||
@@ -803,6 +966,10 @@ void MarkdownArticle::Impl::clearMediaBlocks() {
|
||||
_mediaBlocks.clear();
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::clearPlaceholderRuntimes() {
|
||||
_placeholderRuntimes.clear();
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::refreshMediaBlockHosts() {
|
||||
for (const auto &[id, block] : _mediaBlocks) {
|
||||
if (block) {
|
||||
@@ -811,6 +978,48 @@ void MarkdownArticle::Impl::refreshMediaBlockHosts() {
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<PlaceholderBlockRuntime>
|
||||
MarkdownArticle::Impl::getOrCreatePlaceholderRuntime(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (!id) {
|
||||
return nullptr;
|
||||
}
|
||||
if (const auto i = _placeholderRuntimes.find(id.value);
|
||||
i != end(_placeholderRuntimes)) {
|
||||
return i->second;
|
||||
}
|
||||
auto runtime = std::make_shared<PlaceholderBlockRuntime>([=] {
|
||||
requestPlaceholderRepaint(id);
|
||||
});
|
||||
_placeholderRuntimes.emplace(id.value, runtime);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::prunePlaceholderRuntimes() {
|
||||
auto live = std::unordered_set<uint64>();
|
||||
CollectPlaceholderIds(_blocks, &live);
|
||||
for (auto i = _placeholderRuntimes.begin(); i != _placeholderRuntimes.end();) {
|
||||
if (live.find(i->first) != end(live)) {
|
||||
++i;
|
||||
} else {
|
||||
i = _placeholderRuntimes.erase(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::requestPlaceholderRepaint(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (const auto block = FindPlaceholderBlock(_blocks, id)) {
|
||||
if (_textRepaintRect) {
|
||||
_textRepaintRect(block->mediaRect);
|
||||
} else if (_textRepaint) {
|
||||
_textRepaint();
|
||||
}
|
||||
} else if (_textRepaint) {
|
||||
_textRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<MediaBlock> MarkdownArticle::Impl::getOrCreateMediaBlock(
|
||||
const PreparedBlock &prepared) {
|
||||
switch (prepared.kind) {
|
||||
@@ -955,6 +1164,33 @@ void MarkdownArticle::Impl::resetFormulaRasterCache() {
|
||||
_formulaRenders.resize(_content.formulas.size());
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::setPlaceholderLoadingValue(
|
||||
PreparedPlaceholderBlockId id,
|
||||
bool loading) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const auto runtime = loading
|
||||
? getOrCreatePlaceholderRuntime(id)
|
||||
: [&]() -> std::shared_ptr<PlaceholderBlockRuntime> {
|
||||
if (const auto i = _placeholderRuntimes.find(id.value);
|
||||
i != end(_placeholderRuntimes)) {
|
||||
return i->second;
|
||||
}
|
||||
return nullptr;
|
||||
}();
|
||||
if (!runtime || runtime->loading == loading) {
|
||||
return;
|
||||
}
|
||||
runtime->loading = loading;
|
||||
if (loading) {
|
||||
runtime->loadingAnimation.start();
|
||||
} else {
|
||||
runtime->loadingAnimation.stop(anim::type::instant);
|
||||
}
|
||||
requestPlaceholderRepaint(id);
|
||||
}
|
||||
|
||||
void MarkdownArticle::Impl::relayout(int width) {
|
||||
width = std::max(width, 1);
|
||||
if (_width == width) {
|
||||
@@ -987,6 +1223,9 @@ void MarkdownArticle::Impl::relayout(int width) {
|
||||
context.mediaBlockFactory = [=](const PreparedBlock &prepared) {
|
||||
return getOrCreateMediaBlock(prepared);
|
||||
};
|
||||
context.placeholderRuntimeFactory = [=](PreparedPlaceholderBlockId id) {
|
||||
return getOrCreatePlaceholderRuntime(id);
|
||||
};
|
||||
const auto y = LayoutBlocks(
|
||||
_content.blocks.blocks,
|
||||
&_content.formulas,
|
||||
@@ -999,7 +1238,8 @@ void MarkdownArticle::Impl::relayout(int width) {
|
||||
page.left(),
|
||||
page.top(),
|
||||
innerWidth,
|
||||
std::move(context));
|
||||
context);
|
||||
prunePlaceholderRuntimes();
|
||||
RestoreRelatedArticleThumbnailStates(
|
||||
&_blocks,
|
||||
_relatedArticleThumbnails);
|
||||
@@ -1138,4 +1378,26 @@ MediaBlockHost *MarkdownArticle::mediaBlockHost() const {
|
||||
return _impl->mediaBlockHost();
|
||||
}
|
||||
|
||||
void MarkdownArticle::setPlaceholderLoading(PreparedPlaceholderBlockId id) {
|
||||
_impl->setPlaceholderLoading(id);
|
||||
}
|
||||
|
||||
void MarkdownArticle::clearPlaceholderLoading(PreparedPlaceholderBlockId id) {
|
||||
_impl->clearPlaceholderLoading(id);
|
||||
}
|
||||
|
||||
void MarkdownArticle::clearAllPlaceholderLoading() {
|
||||
_impl->clearAllPlaceholderLoading();
|
||||
}
|
||||
|
||||
void MarkdownArticle::addPlaceholderRipple(
|
||||
PreparedPlaceholderBlockId id,
|
||||
QPoint point) {
|
||||
_impl->addPlaceholderRipple(id, point);
|
||||
}
|
||||
|
||||
void MarkdownArticle::stopPlaceholderRipple(PreparedPlaceholderBlockId id) {
|
||||
_impl->stopPlaceholderRipple(id);
|
||||
}
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -12,6 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "iv/markdown/iv_markdown_prepare.h"
|
||||
|
||||
#include "spellcheck/spellcheck_highlight_syntax.h"
|
||||
#include "ui/click_handler.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
@@ -21,6 +24,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Iv::Markdown {
|
||||
|
||||
struct PlaceholderBlockRuntime {
|
||||
explicit PlaceholderBlockRuntime(Fn<void()> repaint);
|
||||
|
||||
ClickHandlerPtr clickHandler;
|
||||
bool loading = false;
|
||||
Ui::InfiniteRadialAnimation loadingAnimation;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
QSize rippleSize;
|
||||
};
|
||||
|
||||
struct MarkdownArticlePaintCaches {
|
||||
Ui::Text::QuotePaintCache *pre = nullptr;
|
||||
Ui::Text::QuotePaintCache *blockquote = nullptr;
|
||||
@@ -35,6 +48,7 @@ struct MarkdownArticleHitTestResult {
|
||||
Ui::Text::StateResult state;
|
||||
std::optional<PreparedLink> preparedLink;
|
||||
MediaActivation mediaActivation;
|
||||
QPoint placeholderLocalPoint;
|
||||
int forcedOffset = -1;
|
||||
bool direct = false;
|
||||
|
||||
@@ -155,11 +169,17 @@ public:
|
||||
void invalidatePaletteCache();
|
||||
void invalidateRasterCache();
|
||||
[[nodiscard]] MediaBlockHost *mediaBlockHost() const;
|
||||
void setPlaceholderLoading(PreparedPlaceholderBlockId id);
|
||||
void clearPlaceholderLoading(PreparedPlaceholderBlockId id);
|
||||
void clearAllPlaceholderLoading();
|
||||
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
|
||||
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
|
||||
std::unique_ptr<Impl> _impl;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -1108,12 +1108,18 @@ LaidOutBlock LayoutPlaceholderBlock(
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
int width,
|
||||
LayoutContext context) {
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = PreparedBlockKind::Placeholder;
|
||||
block.anchorId = prepared.anchorId;
|
||||
block.copyText = prepared.placeholder.copyText;
|
||||
block.labelText = prepared.placeholder.label;
|
||||
block.placeholderId = prepared.placeholder.id;
|
||||
if (block.placeholderId && context.placeholderRuntimeFactory) {
|
||||
block.placeholderRuntime = context.placeholderRuntimeFactory(
|
||||
block.placeholderId);
|
||||
}
|
||||
block.supplementary = prepared.supplementary;
|
||||
|
||||
const auto &style = markdown.placeholder;
|
||||
@@ -1136,6 +1142,12 @@ LaidOutBlock LayoutPlaceholderBlock(
|
||||
labelHeight + style.padding.top() + style.padding.bottom());
|
||||
block.mediaRect = QRect(left, top, blockWidth, mediaHeight);
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
if (block.placeholderRuntime
|
||||
&& block.placeholderRuntime->ripple
|
||||
&& block.placeholderRuntime->rippleSize != block.mediaRect.size()) {
|
||||
block.placeholderRuntime->ripple = nullptr;
|
||||
block.placeholderRuntime->rippleSize = QSize();
|
||||
}
|
||||
block.labelRect = QRect(
|
||||
contentLeft,
|
||||
top + std::max((mediaHeight - labelHeight) / 2, 0),
|
||||
@@ -1148,6 +1160,7 @@ LaidOutBlock LayoutPlaceholderBlock(
|
||||
if (prepared.placeholder.embed) {
|
||||
block.activation.kind = MediaActivationKind::Embed;
|
||||
block.activation.embed = *prepared.placeholder.embed;
|
||||
block.activation.placeholderId = block.placeholderId;
|
||||
}
|
||||
|
||||
auto bottom = top + mediaHeight;
|
||||
@@ -1160,9 +1173,10 @@ LaidOutBlock LayoutPlaceholderBlock(
|
||||
markdown,
|
||||
contentLeft,
|
||||
bottom,
|
||||
contentWidth,
|
||||
style.captionSkip,
|
||||
&bottom);
|
||||
contentWidth,
|
||||
style.captionSkip,
|
||||
&bottom,
|
||||
context);
|
||||
|
||||
block.contentRect = QRect(
|
||||
left,
|
||||
|
||||
@@ -73,6 +73,7 @@ struct LaidOutBlock {
|
||||
QString codeLanguage;
|
||||
std::optional<PreparedLink> preparedLink;
|
||||
ClickHandlerPtr preparedLinkHandler;
|
||||
PreparedPlaceholderBlockId placeholderId;
|
||||
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0;
|
||||
std::vector<LaidOutBlock> children;
|
||||
std::vector<LaidOutTableRow> tableRows;
|
||||
@@ -118,6 +119,7 @@ struct LaidOutBlock {
|
||||
int secondarySegmentIndex = -1;
|
||||
int tertiarySegmentIndex = -1;
|
||||
std::shared_ptr<MediaBlock> mediaBlock;
|
||||
std::shared_ptr<PlaceholderBlockRuntime> placeholderRuntime;
|
||||
std::shared_ptr<PhotoRuntime> photoRuntime;
|
||||
MediaActivation activation;
|
||||
uint64 thumbnailPhotoId = 0;
|
||||
@@ -140,6 +142,8 @@ struct LayoutContext {
|
||||
bool allowAsyncSyntaxHighlighting = true;
|
||||
CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker = nullptr;
|
||||
std::function<std::shared_ptr<MediaBlock>(const PreparedBlock&)> mediaBlockFactory;
|
||||
std::function<std::shared_ptr<PlaceholderBlockRuntime>(
|
||||
PreparedPlaceholderBlockId)> placeholderRuntimeFactory;
|
||||
};
|
||||
|
||||
struct TableCellLayoutData {
|
||||
@@ -247,7 +251,8 @@ void RepopulateCodeBlockLeaf(
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
int width,
|
||||
LayoutContext context = {});
|
||||
[[nodiscard]] LaidOutBlock LayoutRelatedArticleBlock(
|
||||
const PreparedBlock &prepared,
|
||||
const style::Markdown &markdown,
|
||||
|
||||
@@ -53,7 +53,6 @@ namespace {
|
||||
case PreparedBlockKind::Channel:
|
||||
case PreparedBlockKind::GroupedMedia:
|
||||
case PreparedBlockKind::RelatedArticle:
|
||||
case PreparedBlockKind::Placeholder:
|
||||
return true;
|
||||
case PreparedBlockKind::Paragraph:
|
||||
case PreparedBlockKind::Heading:
|
||||
@@ -65,6 +64,7 @@ namespace {
|
||||
case PreparedBlockKind::DisplayMath:
|
||||
case PreparedBlockKind::Table:
|
||||
case PreparedBlockKind::Details:
|
||||
case PreparedBlockKind::Placeholder:
|
||||
case PreparedBlockKind::EmbedPost:
|
||||
return false;
|
||||
}
|
||||
@@ -904,7 +904,8 @@ void PrepareNestedContext(
|
||||
markdown,
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
width,
|
||||
context);
|
||||
case PreparedBlockKind::Details:
|
||||
return LayoutDetailsBlock(
|
||||
prepared,
|
||||
|
||||
@@ -629,6 +629,7 @@ void PaintQuoteBlock(
|
||||
void PaintPlaceholderBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
int outerWidth,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
@@ -638,28 +639,88 @@ void PaintPlaceholderBlock(
|
||||
p.save();
|
||||
p.setClipRect(visible);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto max = block.labelLeaf.maxWidth();
|
||||
const auto radius = markdown.placeholder.padding.left();
|
||||
p.setBrush(st::windowBgOver);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto skip = (max < block.labelRect.width())
|
||||
? ((block.labelRect.width() - max) / 2)
|
||||
: 0;
|
||||
p.drawRoundedRect(
|
||||
block.labelRect.marginsRemoved(
|
||||
{ skip, 0, skip, 0 }
|
||||
).marginsAdded(markdown.placeholder.padding),
|
||||
radius,
|
||||
radius);
|
||||
p.setPen(st::windowSubTextFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.labelLeaf,
|
||||
caches,
|
||||
block.labelRect,
|
||||
block.labelWidth,
|
||||
visible,
|
||||
style::al_center);
|
||||
if (block.activation.kind == MediaActivationKind::Embed
|
||||
&& block.placeholderRuntime) {
|
||||
const auto border = markdown.placeholder.border;
|
||||
const auto radius = markdown.placeholder.radius;
|
||||
const auto borderSkip = border / 2;
|
||||
const auto borderRect = block.mediaRect.marginsRemoved(QMargins(
|
||||
borderSkip,
|
||||
borderSkip,
|
||||
borderSkip,
|
||||
borderSkip));
|
||||
const auto active = ClickHandler::showAsActive(
|
||||
block.placeholderRuntime->clickHandler);
|
||||
const auto pressed = ClickHandler::showAsPressed(
|
||||
block.placeholderRuntime->clickHandler);
|
||||
if (active || pressed) {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgOver);
|
||||
p.drawRoundedRect(block.mediaRect, radius, radius);
|
||||
}
|
||||
if (const auto &ripple = block.placeholderRuntime->ripple) {
|
||||
ripple->paint(
|
||||
p,
|
||||
block.mediaRect.x(),
|
||||
block.mediaRect.y(),
|
||||
outerWidth,
|
||||
&st::windowBgRipple->c);
|
||||
}
|
||||
auto pen = QPen(st::windowActiveTextFg->c);
|
||||
pen.setWidth(border);
|
||||
p.setPen(pen);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRoundedRect(borderRect, radius, radius);
|
||||
if (block.placeholderRuntime->loading) {
|
||||
const auto size = QSize(
|
||||
markdown.placeholder.spinnerSize,
|
||||
markdown.placeholder.spinnerSize);
|
||||
const auto spinner = style::centerrect(
|
||||
block.mediaRect,
|
||||
QRect(QPoint(), size));
|
||||
Ui::InfiniteRadialAnimation::Draw(
|
||||
p,
|
||||
block.placeholderRuntime->loadingAnimation.computeState(),
|
||||
spinner.topLeft(),
|
||||
spinner.size(),
|
||||
outerWidth,
|
||||
QPen(st::windowActiveTextFg->c),
|
||||
markdown.placeholder.spinnerWidth);
|
||||
} else {
|
||||
p.setPen(st::windowActiveTextFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.labelLeaf,
|
||||
caches,
|
||||
block.labelRect,
|
||||
block.labelWidth,
|
||||
visible,
|
||||
style::al_center);
|
||||
}
|
||||
} else {
|
||||
const auto max = block.labelLeaf.maxWidth();
|
||||
const auto radius = markdown.placeholder.padding.left();
|
||||
p.setBrush(st::windowBgOver);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto skip = (max < block.labelRect.width())
|
||||
? ((block.labelRect.width() - max) / 2)
|
||||
: 0;
|
||||
p.drawRoundedRect(
|
||||
block.labelRect.marginsRemoved(
|
||||
{ skip, 0, skip, 0 }
|
||||
).marginsAdded(markdown.placeholder.padding),
|
||||
radius,
|
||||
radius);
|
||||
p.setPen(st::windowSubTextFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.labelLeaf,
|
||||
caches,
|
||||
block.labelRect,
|
||||
block.labelWidth,
|
||||
visible,
|
||||
style::al_center);
|
||||
}
|
||||
if (block.segmentIndex >= 0
|
||||
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
||||
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
|
||||
@@ -1340,6 +1401,7 @@ void PaintBlock(
|
||||
PaintPlaceholderBlock(
|
||||
p,
|
||||
block,
|
||||
outerWidth,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
|
||||
@@ -88,6 +88,22 @@ public:
|
||||
virtual void join(Qt::MouseButton button) const = 0;
|
||||
};
|
||||
|
||||
struct PreparedMediaBlockId {
|
||||
uint64 value = 0;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return (value != 0);
|
||||
}
|
||||
};
|
||||
|
||||
struct PreparedPlaceholderBlockId {
|
||||
uint64 value = 0;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return (value != 0);
|
||||
}
|
||||
};
|
||||
|
||||
struct PreparedPhotoBlockData;
|
||||
struct PreparedVideoBlockData;
|
||||
struct PreparedAudioBlockData;
|
||||
@@ -197,6 +213,7 @@ struct MediaActivation {
|
||||
MediaActivationKind kind = MediaActivationKind::None;
|
||||
QString url;
|
||||
EmbedRequest embed;
|
||||
PreparedPlaceholderBlockId placeholderId;
|
||||
std::shared_ptr<PhotoRuntime> photo;
|
||||
std::shared_ptr<DocumentRuntime> document;
|
||||
std::shared_ptr<ChannelRuntime> channel;
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "iv/markdown/iv_markdown_embed_overlay.h"
|
||||
|
||||
#include "base/algorithm.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
@@ -40,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Iv::Markdown {
|
||||
namespace {
|
||||
|
||||
constexpr auto kReadyRevealDelay = crl::time(1000);
|
||||
|
||||
[[nodiscard]] TextWithEntities GenericWebviewErrorText() {
|
||||
return { u"Error: Could not initialize WebView."_q };
|
||||
}
|
||||
@@ -211,6 +214,9 @@ EmbedOverlay::EmbedOverlay(
|
||||
, _linkActivationCallback(std::move(linkActivationCallback))
|
||||
, _storageId(std::move(storageId))
|
||||
, _dataRequestHandler(std::move(dataRequestHandler))
|
||||
, _readyDelayTimer([=] {
|
||||
revealReadyEmbed();
|
||||
})
|
||||
, _loadingAnimation(
|
||||
[=] {
|
||||
if (!anim::Disabled()) {
|
||||
@@ -244,17 +250,41 @@ EmbedOverlay::EmbedOverlay(
|
||||
|
||||
EmbedOverlay::~EmbedOverlay() {
|
||||
destroyWebview();
|
||||
cancelReadyDelay();
|
||||
removeEscapeFilter();
|
||||
}
|
||||
|
||||
bool EmbedOverlay::preloadEmbed(
|
||||
const EmbedRequest &request,
|
||||
std::function<void()> shownCallback,
|
||||
std::function<void()> failedCallback) {
|
||||
return startEmbed(
|
||||
request,
|
||||
false,
|
||||
std::move(shownCallback),
|
||||
std::move(failedCallback));
|
||||
}
|
||||
|
||||
bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
|
||||
return startEmbed(request, true, {}, {});
|
||||
}
|
||||
|
||||
void EmbedOverlay::cancelPreload() {
|
||||
closeEmbed();
|
||||
}
|
||||
|
||||
bool EmbedOverlay::startEmbed(
|
||||
const EmbedRequest &request,
|
||||
bool showErrorOnFailure,
|
||||
std::function<void()> shownCallback,
|
||||
std::function<void()> failedCallback) {
|
||||
if (!request) {
|
||||
return false;
|
||||
}
|
||||
closeEmbed();
|
||||
if (isHidden()) {
|
||||
_focusRestore = QApplication::focusWidget();
|
||||
}
|
||||
destroyWebview();
|
||||
_request = request;
|
||||
_preferredBodySize = QSize();
|
||||
_pendingPreferredBodySize = QSize();
|
||||
@@ -263,10 +293,12 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
|
||||
_readyFromResource = false;
|
||||
_ready = false;
|
||||
_loading = true;
|
||||
_showErrorOnFailure = showErrorOnFailure;
|
||||
_shownCallback = std::move(shownCallback);
|
||||
_failedCallback = std::move(failedCallback);
|
||||
clearWebviewError();
|
||||
QWidget::show();
|
||||
installEscapeFilter();
|
||||
raiseSurfaces();
|
||||
QWidget::hide();
|
||||
removeEscapeFilter();
|
||||
updateContentGeometry();
|
||||
_loadingAnimation.start();
|
||||
_content->update();
|
||||
@@ -306,9 +338,7 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
|
||||
}
|
||||
|
||||
void EmbedOverlay::closeEmbed() {
|
||||
if (isHidden()) {
|
||||
return;
|
||||
}
|
||||
cancelReadyDelay();
|
||||
_loading = false;
|
||||
_loadingAnimation.stop(anim::type::instant);
|
||||
if (_content) {
|
||||
@@ -325,6 +355,10 @@ void EmbedOverlay::closeEmbed() {
|
||||
_cssToQtScale = 1.;
|
||||
_readyFromResource = false;
|
||||
_ready = false;
|
||||
_showErrorOnFailure = false;
|
||||
_shownCallback = nullptr;
|
||||
_failedCallback = nullptr;
|
||||
_pressedOutside = false;
|
||||
restoreFocus();
|
||||
}
|
||||
|
||||
@@ -342,11 +376,11 @@ void EmbedOverlay::testHandleWebviewMessage(const QJsonDocument &message) {
|
||||
}
|
||||
|
||||
void EmbedOverlay::testHandleNavigationDone(bool success) {
|
||||
if (success) {
|
||||
if (success && _readyFromResource) {
|
||||
handleWebviewMessage(NavigationReadyMessage(
|
||||
_readyFromResource ? _request.resourceId : QByteArray(),
|
||||
_readyFromResource ? _readyNavigationToken : QString(),
|
||||
_readyFromResource ? QString() : _request.fallbackUrl));
|
||||
_request.resourceId,
|
||||
_readyNavigationToken,
|
||||
QString()));
|
||||
return;
|
||||
}
|
||||
handleNavigationDone(success);
|
||||
@@ -356,6 +390,15 @@ bool EmbedOverlay::testLoadingCoverVisible() const {
|
||||
return _loading;
|
||||
}
|
||||
|
||||
bool EmbedOverlay::testReadyDelayScheduled() const {
|
||||
return _readyDelayTimer.isActive();
|
||||
}
|
||||
|
||||
void EmbedOverlay::testFireReadyDelay() {
|
||||
cancelReadyDelay();
|
||||
revealReadyEmbed();
|
||||
}
|
||||
|
||||
const Webview::StorageId &EmbedOverlay::testEffectiveStorageId() const {
|
||||
return _storageId;
|
||||
}
|
||||
@@ -535,16 +578,15 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
|
||||
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()) {
|
||||
if (!_readyFromResource) {
|
||||
return;
|
||||
}
|
||||
if (object.value("token").toString() != _readyNavigationToken) {
|
||||
return;
|
||||
}
|
||||
if (normalizedRequestId(
|
||||
object.value("resourceId").toString().toStdString())
|
||||
!= _request.resourceId) {
|
||||
return;
|
||||
}
|
||||
setReady();
|
||||
@@ -578,12 +620,22 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
|
||||
}
|
||||
|
||||
void EmbedOverlay::handleNavigationDone(bool success) {
|
||||
if (success || !_request || _ready || isHidden()) {
|
||||
if (!_request || _ready) {
|
||||
return;
|
||||
}
|
||||
if (success) {
|
||||
if (!_readyFromResource) {
|
||||
setReady();
|
||||
}
|
||||
return;
|
||||
}
|
||||
showWebviewError();
|
||||
}
|
||||
|
||||
void EmbedOverlay::cancelReadyDelay() {
|
||||
_readyDelayTimer.cancel();
|
||||
}
|
||||
|
||||
void EmbedOverlay::setReady() {
|
||||
if (_ready || !_webview || !_webview->widget()) {
|
||||
return;
|
||||
@@ -594,19 +646,34 @@ void EmbedOverlay::setReady() {
|
||||
|| _pendingPreferredBodySize.height() > 0)) {
|
||||
_preferredBodySize = _pendingPreferredBodySize;
|
||||
_pendingPreferredBodySize = QSize();
|
||||
updateContentGeometry();
|
||||
} else {
|
||||
updateWebviewGeometry();
|
||||
}
|
||||
updateContentGeometry();
|
||||
cancelReadyDelay();
|
||||
_readyDelayTimer.callOnce(kReadyRevealDelay);
|
||||
}
|
||||
|
||||
void EmbedOverlay::revealReadyEmbed() {
|
||||
if (!_ready || !_request || !_webview || !_webview->widget()) {
|
||||
return;
|
||||
}
|
||||
cancelReadyDelay();
|
||||
clearWebviewError();
|
||||
QWidget::show();
|
||||
installEscapeFilter();
|
||||
raiseSurfaces();
|
||||
updateContentGeometry();
|
||||
_loading = false;
|
||||
_loadingAnimation.stop(anim::type::normal);
|
||||
_content->update();
|
||||
clearWebviewError();
|
||||
_webview->widget()->show();
|
||||
_webview->widget()->raise();
|
||||
_webview->focus();
|
||||
update();
|
||||
_content->update();
|
||||
if (const auto shownCallback = base::take(_shownCallback)) {
|
||||
shownCallback();
|
||||
}
|
||||
_failedCallback = nullptr;
|
||||
}
|
||||
|
||||
void EmbedOverlay::applyPreferredBodySize(QSize size) {
|
||||
@@ -753,11 +820,34 @@ void EmbedOverlay::showWebviewError() {
|
||||
}
|
||||
|
||||
void EmbedOverlay::showWebviewError(const TextWithEntities &text) {
|
||||
cancelReadyDelay();
|
||||
_ready = false;
|
||||
_loading = false;
|
||||
_loadingAnimation.stop(anim::type::normal);
|
||||
_content->update();
|
||||
hideWebview();
|
||||
destroyWebview();
|
||||
_shownCallback = nullptr;
|
||||
const auto failedCallback = base::take(_failedCallback);
|
||||
const auto showError = !isHidden() || _showErrorOnFailure;
|
||||
_showErrorOnFailure = false;
|
||||
_readyNavigationToken = QString();
|
||||
_readyFromResource = false;
|
||||
if (!showError) {
|
||||
clearWebviewError();
|
||||
_request = EmbedRequest();
|
||||
_preferredBodySize = QSize();
|
||||
_pendingPreferredBodySize = QSize();
|
||||
_cssToQtScale = 1.;
|
||||
restoreFocus();
|
||||
if (failedCallback) {
|
||||
failedCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
QWidget::show();
|
||||
installEscapeFilter();
|
||||
raiseSurfaces();
|
||||
if (!_error) {
|
||||
_error = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
_content,
|
||||
@@ -783,6 +873,9 @@ void EmbedOverlay::showWebviewError(const TextWithEntities &text) {
|
||||
_errorLabel->setMarkedText(AddFallbackAction(text, _request.fallbackUrl));
|
||||
_error->show();
|
||||
updateContentGeometry();
|
||||
if (failedCallback) {
|
||||
failedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void EmbedOverlay::clearWebviewError() {
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "base/flat_map.h"
|
||||
#include "base/timer.h"
|
||||
#include "iv/markdown/iv_markdown_common.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/rp_widget.h"
|
||||
@@ -53,12 +54,19 @@ public:
|
||||
dataRequestHandler);
|
||||
~EmbedOverlay();
|
||||
|
||||
[[nodiscard]] bool preloadEmbed(
|
||||
const EmbedRequest &request,
|
||||
std::function<void()> shownCallback = {},
|
||||
std::function<void()> failedCallback = {});
|
||||
[[nodiscard]] bool showEmbed(const EmbedRequest &request);
|
||||
void cancelPreload();
|
||||
void closeEmbed();
|
||||
void updateGeometry(QRect geometry, int contentWidth);
|
||||
void testHandleWebviewMessage(const QJsonDocument &message);
|
||||
void testHandleNavigationDone(bool success);
|
||||
[[nodiscard]] bool testLoadingCoverVisible() const;
|
||||
[[nodiscard]] bool testReadyDelayScheduled() const;
|
||||
void testFireReadyDelay();
|
||||
[[nodiscard]] const Webview::StorageId &testEffectiveStorageId() const;
|
||||
|
||||
protected:
|
||||
@@ -71,11 +79,18 @@ private:
|
||||
void installEscapeFilter();
|
||||
void removeEscapeFilter();
|
||||
[[nodiscard]] bool eventFromOverlayWindow(QObject *object) const;
|
||||
[[nodiscard]] bool startEmbed(
|
||||
const EmbedRequest &request,
|
||||
bool showErrorOnFailure,
|
||||
std::function<void()> shownCallback,
|
||||
std::function<void()> failedCallback);
|
||||
void ensureWebview();
|
||||
[[nodiscard]] Webview::WindowConfig makeWindowConfig() const;
|
||||
void handleWebviewMessage(const QJsonDocument &message);
|
||||
void handleNavigationDone(bool success);
|
||||
void cancelReadyDelay();
|
||||
void setReady();
|
||||
void revealReadyEmbed();
|
||||
void applyPreferredBodySize(QSize size);
|
||||
void applyPreferredBodyHeight(int height);
|
||||
void updateCssToQtScale(int viewportWidth);
|
||||
@@ -106,6 +121,7 @@ private:
|
||||
Ui::PaddingWrap<Ui::FlatLabel> *_error = nullptr;
|
||||
Ui::FlatLabel *_errorLabel = nullptr;
|
||||
std::unique_ptr<Webview::Window> _webview;
|
||||
base::Timer _readyDelayTimer;
|
||||
Ui::InfiniteRadialAnimation _loadingAnimation;
|
||||
EmbedRequest _request;
|
||||
QRect _contentGeometry;
|
||||
@@ -113,6 +129,8 @@ private:
|
||||
QSize _pendingPreferredBodySize;
|
||||
QString _readyNavigationToken;
|
||||
QPointer<QWidget> _focusRestore;
|
||||
std::function<void()> _shownCallback;
|
||||
std::function<void()> _failedCallback;
|
||||
int _contentWidth = 0;
|
||||
int _navigationGeneration = 0;
|
||||
int _webviewGeneration = 0;
|
||||
@@ -121,6 +139,7 @@ private:
|
||||
bool _readyFromResource = false;
|
||||
bool _ready = false;
|
||||
bool _loading = false;
|
||||
bool _showErrorOnFailure = false;
|
||||
bool _escapeFilterInstalled = false;
|
||||
|
||||
};
|
||||
|
||||
@@ -116,14 +116,6 @@ struct PreparedTableRow {
|
||||
bool header = false;
|
||||
};
|
||||
|
||||
struct PreparedMediaBlockId {
|
||||
uint64 value = 0;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return (value != 0);
|
||||
}
|
||||
};
|
||||
|
||||
struct PreparedPhotoBlockData {
|
||||
PreparedMediaBlockId id;
|
||||
uint64 photoId = 0;
|
||||
@@ -194,6 +186,7 @@ struct PreparedGroupedMediaBlockData {
|
||||
};
|
||||
|
||||
struct PreparedPlaceholderBlockData {
|
||||
PreparedPlaceholderBlockId id;
|
||||
QString label;
|
||||
QString copyText;
|
||||
std::optional<EmbedRequest> embed;
|
||||
|
||||
@@ -765,10 +765,11 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
|
||||
const MTPDpageBlockEmbed &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state) {
|
||||
const auto label = tr::lng_iv_click_to_view(tr::now);
|
||||
const auto html = RenderNativeIvEmbedHtml(data, state, false);
|
||||
if (html.isEmpty()) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Embed Placeholder"_q,
|
||||
label,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
@@ -787,7 +788,7 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
|
||||
.allowScrolling = data.is_allow_scrolling(),
|
||||
};
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Embed Placeholder"_q,
|
||||
label,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state,
|
||||
@@ -1506,6 +1507,9 @@ void MarkNativeIvTableSlots(
|
||||
data.vtext(),
|
||||
state);
|
||||
}, [&](const MTPDpageBlockAuthorDate &data) {
|
||||
return PrepareNativeIvPlainPlaceholderBlock(
|
||||
u"Unsupported Content"_q,
|
||||
result); AssertIsDebug();
|
||||
auto prepared = PreparedIvRichText();
|
||||
auto anchorId = QString();
|
||||
if (!PrepareNativeIvRichText(
|
||||
|
||||
@@ -20,9 +20,19 @@ struct GeoPointLocation;
|
||||
namespace Iv::Markdown {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] uint64 GeneratePreparedBlockIdValue(
|
||||
NativeIvPrepareState *state) {
|
||||
return uint64(++state->nextGeneratedId);
|
||||
}
|
||||
|
||||
[[nodiscard]] PreparedMediaBlockId GeneratePreparedMediaBlockId(
|
||||
NativeIvPrepareState *state) {
|
||||
return { .value = uint64(++state->nextGeneratedId) };
|
||||
return { .value = GeneratePreparedBlockIdValue(state) };
|
||||
}
|
||||
|
||||
[[nodiscard]] PreparedPlaceholderBlockId GeneratePreparedPlaceholderBlockId(
|
||||
NativeIvPrepareState *state) {
|
||||
return { .value = GeneratePreparedBlockIdValue(state) };
|
||||
}
|
||||
|
||||
void ShiftEntities(EntitiesInText *entities, int delta) {
|
||||
@@ -850,8 +860,11 @@ bool PrepareNativeIvPlaceholderBlock(
|
||||
block.links = std::move(prepared.links);
|
||||
block.anchorId = std::move(anchorId);
|
||||
block.supplementary = true;
|
||||
block.placeholder.label = label;
|
||||
block.placeholder.label = std::move(label);
|
||||
block.placeholder.embed = std::move(embed);
|
||||
if (block.placeholder.embed && *block.placeholder.embed) {
|
||||
block.placeholder.id = GeneratePreparedPlaceholderBlockId(state);
|
||||
}
|
||||
block.placeholder.copyText = NativeIvPlaceholderCopyText(
|
||||
block.placeholder.label,
|
||||
block.text);
|
||||
|
||||
@@ -173,9 +173,15 @@ public:
|
||||
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
|
||||
|
||||
private:
|
||||
struct PendingEmbedState {
|
||||
PreparedPlaceholderBlockId placeholderId;
|
||||
uint64 generation = 0;
|
||||
};
|
||||
|
||||
void setup();
|
||||
void prepareArticle();
|
||||
void activateLink(const PreparedLink &link, Qt::MouseButton button);
|
||||
void closeEmbed();
|
||||
void openEmbedLink(QString url);
|
||||
void showFootnote(const PreparedLink &link, Qt::MouseButton button);
|
||||
[[nodiscard]] bool showEmbed(const MediaActivation &activation);
|
||||
@@ -208,6 +214,7 @@ private:
|
||||
std::shared_ptr<MarkdownArticle> _article;
|
||||
QString _pendingFragment;
|
||||
int _devicePixelRatio = 0;
|
||||
PendingEmbedState _pendingEmbed;
|
||||
|
||||
};
|
||||
|
||||
@@ -430,13 +437,27 @@ void MarkdownPreviewRoot::activateLink(
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownPreviewRoot::closeEmbed() {
|
||||
if (_body) {
|
||||
_body->clearAllPlaceholderLoading();
|
||||
}
|
||||
const auto hadPending = bool(_pendingEmbed.placeholderId);
|
||||
_pendingEmbed.placeholderId = {};
|
||||
if (!_embedOverlay) {
|
||||
return;
|
||||
}
|
||||
if (hadPending) {
|
||||
_embedOverlay->cancelPreload();
|
||||
} else {
|
||||
_embedOverlay->closeEmbed();
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownPreviewRoot::openEmbedLink(QString url) {
|
||||
if (url.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (_embedOverlay) {
|
||||
_embedOverlay->closeEmbed();
|
||||
}
|
||||
closeEmbed();
|
||||
HiddenUrlClickHandler::Open(url, CurrentClickHandlerContext(_options));
|
||||
}
|
||||
|
||||
@@ -464,9 +485,45 @@ void MarkdownPreviewRoot::showFootnote(
|
||||
}
|
||||
|
||||
bool MarkdownPreviewRoot::showEmbed(const MediaActivation &activation) {
|
||||
return _embedOverlay
|
||||
? _embedOverlay->showEmbed(activation.embed)
|
||||
: false;
|
||||
if (activation.kind != MediaActivationKind::Embed
|
||||
|| !activation.embed
|
||||
|| !activation.placeholderId) {
|
||||
return false;
|
||||
}
|
||||
const auto placeholderId = activation.placeholderId;
|
||||
const auto generation = ++_pendingEmbed.generation;
|
||||
if (_body && _pendingEmbed.placeholderId) {
|
||||
_body->clearPlaceholderLoading(_pendingEmbed.placeholderId);
|
||||
}
|
||||
if (_pendingEmbed.placeholderId && _embedOverlay) {
|
||||
_embedOverlay->cancelPreload();
|
||||
}
|
||||
_pendingEmbed.placeholderId = placeholderId;
|
||||
if (_body) {
|
||||
_body->setPlaceholderLoading(placeholderId);
|
||||
}
|
||||
const auto finishPending = [=] {
|
||||
if (_pendingEmbed.generation != generation
|
||||
|| (_pendingEmbed.placeholderId.value != placeholderId.value)) {
|
||||
return;
|
||||
}
|
||||
if (_body) {
|
||||
_body->clearPlaceholderLoading(placeholderId);
|
||||
}
|
||||
_pendingEmbed.placeholderId = {};
|
||||
};
|
||||
if (!_embedOverlay) {
|
||||
finishPending();
|
||||
return false;
|
||||
}
|
||||
const auto started = _embedOverlay->preloadEmbed(
|
||||
activation.embed,
|
||||
finishPending,
|
||||
finishPending);
|
||||
if (!started) {
|
||||
finishPending();
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
void MarkdownPreviewRoot::fillFootnoteBox(
|
||||
@@ -509,9 +566,7 @@ void MarkdownPreviewRoot::applyPreparedContent(
|
||||
int prepareMs) {
|
||||
const auto failure = prepared.failure;
|
||||
const auto debug = prepared.debug;
|
||||
if (_embedOverlay) {
|
||||
_embedOverlay->closeEmbed();
|
||||
}
|
||||
closeEmbed();
|
||||
if (failure.failed()) {
|
||||
_article = nullptr;
|
||||
_footnotes.clear();
|
||||
|
||||
@@ -322,6 +322,41 @@ void MarkdownDocumentWidget::requestRelayout(QRect articleRect) {
|
||||
});
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::setPlaceholderLoading(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (_article) {
|
||||
_article->setPlaceholderLoading(id);
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::clearPlaceholderLoading(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (_article) {
|
||||
_article->clearPlaceholderLoading(id);
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::clearAllPlaceholderLoading() {
|
||||
if (_article) {
|
||||
_article->clearAllPlaceholderLoading();
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::addPlaceholderRipple(
|
||||
PreparedPlaceholderBlockId id,
|
||||
QPoint point) {
|
||||
if (_article) {
|
||||
_article->addPlaceholderRipple(id, point);
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::stopPlaceholderRipple(
|
||||
PreparedPlaceholderBlockId id) {
|
||||
if (_article) {
|
||||
_article->stopPlaceholderRipple(id);
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::paintEvent(QPaintEvent *e) {
|
||||
if (!_article) {
|
||||
return;
|
||||
@@ -519,6 +554,7 @@ void MarkdownDocumentWidget::mouseDoubleClickEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::focusOutEvent(QFocusEvent *e) {
|
||||
stopPressedPlaceholderRipple();
|
||||
if (!_selection.empty()) {
|
||||
_savedSelection = _selection;
|
||||
_savedSelectionEndpoints = _selectionEndpoints;
|
||||
@@ -868,9 +904,17 @@ MarkdownArticlePaintCaches MarkdownDocumentWidget::textPaintCaches() {
|
||||
};
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::stopPressedPlaceholderRipple() {
|
||||
if (_pressedPlaceholderId) {
|
||||
stopPlaceholderRipple(_pressedPlaceholderId);
|
||||
_pressedPlaceholderId = {};
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::dragActionStart(
|
||||
QPoint point,
|
||||
Qt::MouseButton button) {
|
||||
stopPressedPlaceholderRipple();
|
||||
const auto state = hitTest(
|
||||
point,
|
||||
Ui::Text::StateRequest::Flag::LookupLink
|
||||
@@ -879,6 +923,13 @@ void MarkdownDocumentWidget::dragActionStart(
|
||||
if (button != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
if (state.mediaActivation.kind == MediaActivationKind::Embed
|
||||
&& state.mediaActivation.placeholderId) {
|
||||
_pressedPlaceholderId = state.mediaActivation.placeholderId;
|
||||
addPlaceholderRipple(
|
||||
state.mediaActivation.placeholderId,
|
||||
state.placeholderLocalPoint);
|
||||
}
|
||||
_dragStartPosition = point;
|
||||
_dragStartHadSelection = !selectionForCopy().empty();
|
||||
_selectionClickPreparedLink = (state.preparedLink
|
||||
@@ -933,6 +984,7 @@ MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
|
||||
QPoint point,
|
||||
Qt::MouseButton button) {
|
||||
const auto state = dragActionUpdate(point);
|
||||
stopPressedPlaceholderRipple();
|
||||
auto activated = ClickHandler::unpressed();
|
||||
const auto dragStartHadSelection = _dragStartHadSelection;
|
||||
const auto toggleFromDetailsClick = !dragStartHadSelection
|
||||
|
||||
@@ -57,6 +57,11 @@ public:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void requestRepaint(QRect articleRect) override;
|
||||
void requestRelayout(QRect articleRect) override;
|
||||
void setPlaceholderLoading(PreparedPlaceholderBlockId id);
|
||||
void clearPlaceholderLoading(PreparedPlaceholderBlockId id);
|
||||
void clearAllPlaceholderLoading();
|
||||
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
|
||||
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
@@ -112,6 +117,7 @@ private:
|
||||
[[nodiscard]] Ui::Text::QuotePaintCache *ensurePrePaintCache();
|
||||
[[nodiscard]] Ui::Text::QuotePaintCache *ensureBlockquotePaintCache();
|
||||
[[nodiscard]] MarkdownArticlePaintCaches textPaintCaches();
|
||||
void stopPressedPlaceholderRipple();
|
||||
void dragActionStart(QPoint point, Qt::MouseButton button);
|
||||
MarkdownArticleHitTestResult dragActionUpdate(QPoint point);
|
||||
MarkdownArticleHitTestResult dragActionFinish(
|
||||
@@ -141,6 +147,7 @@ private:
|
||||
int _dragSymbol = 0;
|
||||
TextSelection _dragExpandedSelection;
|
||||
std::optional<PreparedLink> _selectionClickPreparedLink;
|
||||
PreparedPlaceholderBlockId _pressedPlaceholderId;
|
||||
bool _dragStartHadSelection = false;
|
||||
int _lastRelayoutMs = 0;
|
||||
int _zoom = 100;
|
||||
|
||||
@@ -1243,7 +1243,7 @@ constexpr auto kNativeIvEmbedPostAuthorPhotoId = uint64(9301);
|
||||
case NativeIvPlaceholderKind::Video:
|
||||
return u"Video Placeholder"_q;
|
||||
case NativeIvPlaceholderKind::Embed:
|
||||
return u"Embed Placeholder"_q;
|
||||
return u"Click to View"_q;
|
||||
case NativeIvPlaceholderKind::Collage:
|
||||
return u"Collage placeholder"_q;
|
||||
case NativeIvPlaceholderKind::Slideshow:
|
||||
@@ -4208,6 +4208,8 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
|
||||
const PreparedBlock *embedPost = nullptr;
|
||||
const PreparedBlock *coveredPhoto = nullptr;
|
||||
auto preparedPlaceholders = std::vector<const PreparedBlock*>();
|
||||
auto preparedPlaceholderIds = std::vector<
|
||||
std::pair<QString, PreparedPlaceholderBlockId>>();
|
||||
ForEachPreparedBlock(
|
||||
supported.content.blocks.blocks,
|
||||
[&](const PreparedBlock &block) {
|
||||
@@ -5207,12 +5209,19 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
|
||||
== (fixture.expectedLabel + u"\n"_q + fixture.caption),
|
||||
fixture.expectedLabel + u" placeholder copy text"_q,
|
||||
ok);
|
||||
Check(
|
||||
bool(block->placeholder.id),
|
||||
fixture.expectedLabel + u" placeholder id"_q,
|
||||
ok);
|
||||
Check(
|
||||
block->placeholder.embed.has_value(),
|
||||
fixture.expectedLabel + u" placeholder embed metadata"_q,
|
||||
ok);
|
||||
if (block->placeholder.embed) {
|
||||
const auto &embed = *block->placeholder.embed;
|
||||
preparedPlaceholderIds.emplace_back(
|
||||
embed.fallbackUrl,
|
||||
block->placeholder.id);
|
||||
Check(
|
||||
!embed.resourceId.isEmpty(),
|
||||
fixture.expectedLabel + u" placeholder embed resource id"_q,
|
||||
@@ -5470,6 +5479,12 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
|
||||
const auto hit = placeholderArticle->hitTest(
|
||||
bounds->center(),
|
||||
lookupFlags);
|
||||
const auto preparedId = std::find_if(
|
||||
preparedPlaceholderIds.begin(),
|
||||
preparedPlaceholderIds.end(),
|
||||
[&](const auto &entry) {
|
||||
return entry.first == fixture.expectedFallbackUrl;
|
||||
});
|
||||
Check(
|
||||
hit.mediaActivation.kind == MediaActivationKind::Embed,
|
||||
fixture.expectedLabel + u" article embed activation kind"_q,
|
||||
@@ -5484,22 +5499,40 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
|
||||
== fixture.expectedAllowScrolling,
|
||||
fixture.expectedLabel + u" article embed activation scrolling"_q,
|
||||
ok);
|
||||
Check(
|
||||
bool(hit.mediaActivation.placeholderId),
|
||||
fixture.expectedLabel + u" article embed placeholder id"_q,
|
||||
ok);
|
||||
Check(
|
||||
(preparedId != preparedPlaceholderIds.end())
|
||||
&& (hit.mediaActivation.placeholderId.value
|
||||
== preparedId->second.value),
|
||||
fixture.expectedLabel + u" article embed placeholder id match"_q,
|
||||
ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto makePreviewEmbedBlock = [](
|
||||
QString url,
|
||||
QString caption) {
|
||||
return MTP_pageBlockEmbed(
|
||||
MTP_flags(
|
||||
MTPDpageBlockEmbed::Flag::f_allow_scrolling
|
||||
| MTPDpageBlockEmbed::Flag::f_url
|
||||
| MTPDpageBlockEmbed::Flag::f_w),
|
||||
MTP_string(url),
|
||||
MTP_string(),
|
||||
MTP_long(0),
|
||||
MTP_int(640),
|
||||
MTP_int(0),
|
||||
NativeIvCaption(caption));
|
||||
};
|
||||
|
||||
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));
|
||||
const auto previewEmbedBlock = makePreviewEmbedBlock(
|
||||
u"https://example.com/embed"_q,
|
||||
u"Embed caption"_q);
|
||||
auto previewEmbedSource = NativeIvSource(QVector<MTPPageBlock>{
|
||||
previewEmbedBlock,
|
||||
});
|
||||
@@ -5605,20 +5638,28 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
|
||||
previewEmbedLabel + u" click bounds"_q,
|
||||
ok);
|
||||
if (clickBounds) {
|
||||
const auto bodyBeforeClick = RenderWidgetForTest(body);
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay hidden before click"_q,
|
||||
ok);
|
||||
SendMouseClick(body, clickBounds->center(), Qt::LeftButton);
|
||||
FlushPendingWidgetEvents();
|
||||
const auto loadingBody = RenderWidgetForTest(body);
|
||||
Check(
|
||||
overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay visible after click"_q,
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay hidden after click"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewEmbedLabel
|
||||
+ u" loading cover visible before navigation done"_q,
|
||||
previewEmbedLabel + u" preload active after click"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
bodyBeforeClick,
|
||||
loadingBody,
|
||||
*clickBounds),
|
||||
previewEmbedLabel + u" placeholder loading visible"_q,
|
||||
ok);
|
||||
const auto availableRect = NativeIvOverlayAvailableRect(
|
||||
overlay);
|
||||
@@ -5626,148 +5667,387 @@ 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));
|
||||
NativeIvPreferredSizeMessage(QSize(180, 96)));
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay stays hidden after preferred size"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testReadyDelayScheduled(),
|
||||
previewEmbedLabel + u" ready delay waits for readiness"_q,
|
||||
ok);
|
||||
overlay->testHandleNavigationDone(true);
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay stays hidden after readiness"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewEmbedLabel + u" preload stays active after readiness"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testReadyDelayScheduled(),
|
||||
previewEmbedLabel + u" ready delay scheduled after readiness"_q,
|
||||
ok);
|
||||
overlay->testHandleNavigationDone(false);
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel
|
||||
+ u" late navigation failure keeps ready preload hidden"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewEmbedLabel
|
||||
+ u" loading cover persists after preferred size"_q,
|
||||
+ u" late navigation failure keeps preload active"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlayShell->geometry() == loadingGeometry,
|
||||
overlay->testReadyDelayScheduled(),
|
||||
previewEmbedLabel
|
||||
+ u" overlay defers preferred size while loading"_q,
|
||||
+ u" late navigation failure keeps ready delay"_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);
|
||||
}
|
||||
}
|
||||
const auto readyGeometry = overlayShell->geometry();
|
||||
if (availableRect.isValid()) {
|
||||
overlay->testHandleWebviewMessage(
|
||||
NativeIvPreferredSizeMessage(QSize(
|
||||
availableRect.width() * 2,
|
||||
availableRect.height() * 2)));
|
||||
FlushPendingWidgetEvents();
|
||||
}
|
||||
const auto settledGeometry = overlayShell->geometry();
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel
|
||||
+ u" overlay stays hidden during settled resize"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testReadyDelayScheduled(),
|
||||
previewEmbedLabel + u" ready delay remains scheduled"_q,
|
||||
ok);
|
||||
if (availableRect.isValid()) {
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
readyGeometry != settledGeometry,
|
||||
previewEmbedLabel
|
||||
+ u" loading cover persists before navigation complete"_q,
|
||||
+ u" hidden preload accepts later preferred size"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlayShell->geometry() == loadingGeometry,
|
||||
settledGeometry == availableRect,
|
||||
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 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,
|
||||
+ u" hidden preload clamps latest preferred size"_q,
|
||||
ok);
|
||||
}
|
||||
overlay->testFireReadyDelay();
|
||||
FlushPendingWidgetEvents();
|
||||
const auto revealedBody = RenderWidgetForTest(body);
|
||||
Check(
|
||||
overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay reveals after ready delay"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testLoadingCoverVisible(),
|
||||
previewEmbedLabel + u" preload clears after reveal"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testReadyDelayScheduled(),
|
||||
previewEmbedLabel + u" ready delay clears after reveal"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlayShell->geometry() == settledGeometry,
|
||||
previewEmbedLabel
|
||||
+ u" reveal uses latest settled geometry"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
loadingBody,
|
||||
revealedBody,
|
||||
*clickBounds),
|
||||
previewEmbedLabel + u" placeholder loading clears on reveal"_q,
|
||||
ok);
|
||||
overlay->testHandleNavigationDone(false);
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
overlayShell->isVisible(),
|
||||
previewEmbedLabel
|
||||
+ u" late navigation failure keeps revealed overlay"_q,
|
||||
ok);
|
||||
overlay->closeEmbed();
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay hides before failure retry"_q,
|
||||
ok);
|
||||
const auto bodyBeforeFailureClick = RenderWidgetForTest(body);
|
||||
SendMouseClick(body, clickBounds->center(), Qt::LeftButton);
|
||||
FlushPendingWidgetEvents();
|
||||
const auto failureLoadingBody = RenderWidgetForTest(body);
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" overlay stays hidden on failure click"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewEmbedLabel + u" preload active before failure"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
bodyBeforeFailureClick,
|
||||
failureLoadingBody,
|
||||
*clickBounds),
|
||||
previewEmbedLabel + u" placeholder loading visible on retry"_q,
|
||||
ok);
|
||||
overlay->testHandleNavigationDone(false);
|
||||
FlushPendingWidgetEvents();
|
||||
const auto bodyAfterFailure = RenderWidgetForTest(body);
|
||||
const auto overlayError = preview->findChild<QWidget*>(
|
||||
u"nativeIvEmbedOverlayErrorWrap"_q);
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewEmbedLabel + u" preload failure stays hidden"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testLoadingCoverVisible(),
|
||||
previewEmbedLabel + u" preload clears after failure"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testReadyDelayScheduled(),
|
||||
previewEmbedLabel + u" ready delay clears after failure"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
failureLoadingBody,
|
||||
bodyAfterFailure,
|
||||
*clickBounds),
|
||||
previewEmbedLabel + u" placeholder loading clears on failure"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlayError || overlayError->isHidden(),
|
||||
previewEmbedLabel + u" preload failure keeps overlay error hidden"_q,
|
||||
ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto previewCancelLabel = u"native-iv-embed-preview-cancel"_q;
|
||||
const auto firstPreviewBlock = makePreviewEmbedBlock(
|
||||
u"https://example.com/embed-first"_q,
|
||||
u"First embed"_q);
|
||||
const auto secondPreviewBlock = makePreviewEmbedBlock(
|
||||
u"https://example.com/embed-second"_q,
|
||||
u"Second embed"_q);
|
||||
auto previewCancelSource = NativeIvSource(QVector<MTPPageBlock>{
|
||||
firstPreviewBlock,
|
||||
secondPreviewBlock,
|
||||
});
|
||||
const auto previewCancelPrepared = TryPrepareNativeInstantView({
|
||||
.source = &previewCancelSource,
|
||||
});
|
||||
Check(
|
||||
previewCancelPrepared.supported(),
|
||||
previewCancelLabel + u" prepare supported"_q,
|
||||
ok);
|
||||
Check(
|
||||
!previewCancelPrepared.content.failure.failed(),
|
||||
previewCancelLabel + u" prepare failure"_q,
|
||||
ok);
|
||||
if (previewCancelPrepared.supported()
|
||||
&& !previewCancelPrepared.content.failure.failed()) {
|
||||
auto window = Ui::RpWindow();
|
||||
window.setGeometry(QRect(0, 0, 420, 400));
|
||||
window.show();
|
||||
FlushPendingWidgetEvents();
|
||||
auto previewOptions = OpenOptions();
|
||||
previewOptions.ivWebviewStorageId = {
|
||||
u"native-iv-preview-cancel"_q,
|
||||
QByteArray("phase-5"),
|
||||
};
|
||||
auto preview = CreateMarkdownPreviewWidget(
|
||||
window.body(),
|
||||
std::move(previewCancelPrepared.content),
|
||||
std::make_shared<MathRenderer>(),
|
||||
[](Event) {
|
||||
},
|
||||
previewOptions);
|
||||
preview->setGeometry(QRect(QPoint(), window.body()->size()));
|
||||
preview->show();
|
||||
FlushPendingWidgetEvents();
|
||||
const auto body = FindChildObject<MarkdownDocumentWidget>(preview.get());
|
||||
const auto overlay = preview->findChild<EmbedOverlay*>(
|
||||
u"nativeIvEmbedOverlay"_q);
|
||||
const auto overlayShell = preview->findChild<QWidget*>(
|
||||
u"nativeIvEmbedOverlayShell"_q);
|
||||
Check(
|
||||
body != nullptr,
|
||||
previewCancelLabel + u" preview body widget"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay != nullptr,
|
||||
previewCancelLabel + u" overlay object"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlayShell != nullptr,
|
||||
previewCancelLabel + u" overlay shell object"_q,
|
||||
ok);
|
||||
if (body && overlay && overlayShell) {
|
||||
auto previewProbeSource = NativeIvSource(QVector<MTPPageBlock>{
|
||||
firstPreviewBlock,
|
||||
secondPreviewBlock,
|
||||
});
|
||||
const auto previewProbePrepared = TryPrepareNativeInstantView({
|
||||
.source = &previewProbeSource,
|
||||
});
|
||||
Check(
|
||||
previewProbePrepared.supported(),
|
||||
previewCancelLabel + u" probe prepare supported"_q,
|
||||
ok);
|
||||
Check(
|
||||
!previewProbePrepared.content.failure.failed(),
|
||||
previewCancelLabel + u" probe prepare failure"_q,
|
||||
ok);
|
||||
if (previewProbePrepared.supported()
|
||||
&& !previewProbePrepared.content.failure.failed()) {
|
||||
auto probeHeight = 0;
|
||||
auto probeArticle = BuildArticleForTest(
|
||||
std::move(previewProbePrepared.content),
|
||||
std::make_shared<MathRenderer>(),
|
||||
body->width(),
|
||||
&probeHeight);
|
||||
auto lookupFlags = Ui::Text::StateRequest::Flags();
|
||||
lookupFlags |= Ui::Text::StateRequest::Flag::LookupSymbol;
|
||||
const auto firstBounds = HitBoundsWhere(
|
||||
probeArticle.get(),
|
||||
body->width(),
|
||||
probeHeight,
|
||||
lookupFlags,
|
||||
[](const MarkdownArticleHitTestResult &hit) {
|
||||
return hit.mediaActivation.kind == MediaActivationKind::Embed
|
||||
&& (hit.mediaActivation.embed.fallbackUrl
|
||||
== u"https://example.com/embed-first"_q);
|
||||
});
|
||||
const auto secondBounds = HitBoundsWhere(
|
||||
probeArticle.get(),
|
||||
body->width(),
|
||||
probeHeight,
|
||||
lookupFlags,
|
||||
[](const MarkdownArticleHitTestResult &hit) {
|
||||
return hit.mediaActivation.kind == MediaActivationKind::Embed
|
||||
&& (hit.mediaActivation.embed.fallbackUrl
|
||||
== u"https://example.com/embed-second"_q);
|
||||
});
|
||||
Check(
|
||||
firstBounds.has_value(),
|
||||
previewCancelLabel + u" first click bounds"_q,
|
||||
ok);
|
||||
Check(
|
||||
secondBounds.has_value(),
|
||||
previewCancelLabel + u" second click bounds"_q,
|
||||
ok);
|
||||
if (firstBounds && secondBounds) {
|
||||
const auto bodyBeforeFirstClick = RenderWidgetForTest(body);
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewCancelLabel + u" overlay hidden before first click"_q,
|
||||
ok);
|
||||
SendMouseClick(body, firstBounds->center(), Qt::LeftButton);
|
||||
FlushPendingWidgetEvents();
|
||||
const auto firstLoadingBody = RenderWidgetForTest(body);
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewCancelLabel + u" overlay hidden after first click"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewCancelLabel + u" first preload active"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
bodyBeforeFirstClick,
|
||||
firstLoadingBody,
|
||||
*firstBounds),
|
||||
previewCancelLabel + u" first placeholder loading visible"_q,
|
||||
ok);
|
||||
overlay->testHandleNavigationDone(true);
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
overlay->testReadyDelayScheduled(),
|
||||
previewCancelLabel + u" first ready delay scheduled"_q,
|
||||
ok);
|
||||
SendMouseClick(body, secondBounds->center(), Qt::LeftButton);
|
||||
FlushPendingWidgetEvents();
|
||||
const auto secondLoadingBody = RenderWidgetForTest(body);
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewCancelLabel + u" overlay hidden after second click"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewCancelLabel + u" second preload active"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testReadyDelayScheduled(),
|
||||
previewCancelLabel + u" first ready delay canceled"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
firstLoadingBody,
|
||||
secondLoadingBody,
|
||||
*firstBounds),
|
||||
previewCancelLabel + u" first placeholder loading cleared"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
firstLoadingBody,
|
||||
secondLoadingBody,
|
||||
*secondBounds),
|
||||
previewCancelLabel + u" second placeholder loading visible"_q,
|
||||
ok);
|
||||
overlay->testFireReadyDelay();
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
!overlayShell->isVisible(),
|
||||
previewCancelLabel + u" stale first ready delay does not reveal"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlay->testLoadingCoverVisible(),
|
||||
previewCancelLabel + u" second preload stays active"_q,
|
||||
ok);
|
||||
overlay->testHandleWebviewMessage(
|
||||
NativeIvPreferredSizeMessage(QSize(220, 120)));
|
||||
FlushPendingWidgetEvents();
|
||||
overlay->testHandleNavigationDone(true);
|
||||
FlushPendingWidgetEvents();
|
||||
Check(
|
||||
overlay->testReadyDelayScheduled(),
|
||||
previewCancelLabel + u" second ready delay scheduled"_q,
|
||||
ok);
|
||||
const auto secondSettledGeometry = overlayShell->geometry();
|
||||
overlay->testFireReadyDelay();
|
||||
FlushPendingWidgetEvents();
|
||||
const auto revealedBody = RenderWidgetForTest(body);
|
||||
Check(
|
||||
overlayShell->isVisible(),
|
||||
previewCancelLabel + u" second preload reveals"_q,
|
||||
ok);
|
||||
Check(
|
||||
!overlay->testLoadingCoverVisible(),
|
||||
previewCancelLabel + u" second preload clears after reveal"_q,
|
||||
ok);
|
||||
Check(
|
||||
overlayShell->geometry() == secondSettledGeometry,
|
||||
previewCancelLabel + u" second reveal uses settled geometry"_q,
|
||||
ok);
|
||||
Check(
|
||||
PixelsDifferInRect(
|
||||
secondLoadingBody,
|
||||
revealedBody,
|
||||
*secondBounds),
|
||||
previewCancelLabel + u" second placeholder loading clears"_q,
|
||||
ok);
|
||||
overlay->closeEmbed();
|
||||
FlushPendingWidgetEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user