mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-07 04:05:17 +00:00
1178 lines
26 KiB
C++
1178 lines
26 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "iv/markdown/iv_markdown_article_paint.h"
|
|
#include "iv/markdown/iv_markdown_article_text.h"
|
|
#include "ui/widgets/checkbox.h"
|
|
#include "ui/dynamic_image.h"
|
|
|
|
#include "styles/palette.h"
|
|
#include "styles/style_iv.h"
|
|
#include "styles/style_widgets.h"
|
|
|
|
#include <QtGui/QPainterPath>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace Iv::Markdown {
|
|
namespace {
|
|
|
|
[[nodiscard]] bool PaintDynamicImage(
|
|
Painter &p,
|
|
const std::shared_ptr<Ui::DynamicImage> &image,
|
|
QRect rect) {
|
|
if (!image || rect.isEmpty()) {
|
|
return false;
|
|
}
|
|
if (const auto frame = image->image(std::max(rect.width(), rect.height()));
|
|
!frame.isNull()) {
|
|
p.drawImage(rect, frame);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SubscribeDynamicImage(
|
|
const std::shared_ptr<Ui::DynamicImage> &image,
|
|
const Fn<void()> &repaint,
|
|
bool *subscribed) {
|
|
if (!image || !repaint || *subscribed) {
|
|
return;
|
|
}
|
|
*subscribed = true;
|
|
image->subscribeToUpdates(repaint);
|
|
}
|
|
|
|
void PaintTextLeaf(
|
|
Painter &p,
|
|
const Ui::Text::String &leaf,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
QRect rect,
|
|
int width,
|
|
QRect clip,
|
|
style::align align = style::al_left,
|
|
std::optional<TextSelection> selection = std::nullopt) {
|
|
const auto availableWidth = std::max(width, 1);
|
|
leaf.draw(p, {
|
|
.position = rect.topLeft(),
|
|
.availableWidth = availableWidth,
|
|
.geometry = TextGeometry(availableWidth),
|
|
.align = align,
|
|
.clip = clip,
|
|
.palette = &p.textPalette(),
|
|
.pre = caches.pre,
|
|
.blockquote = caches.blockquote,
|
|
.colors = caches.colors,
|
|
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
|
.now = crl::now(),
|
|
.selection = selection.value_or(TextSelection()),
|
|
});
|
|
}
|
|
|
|
void PaintTaskMarker(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
int outerWidth) {
|
|
const auto rect = block.markerRect;
|
|
if (rect.isEmpty()) {
|
|
return;
|
|
}
|
|
auto view = Ui::CheckView(
|
|
markdown.list.taskCheck,
|
|
block.taskState == TaskState::Checked);
|
|
view.finishAnimating();
|
|
view.paint(p, rect.left(), rect.top(), outerWidth);
|
|
}
|
|
|
|
void PaintBulletMarker(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown) {
|
|
const auto radius = markdown.list.bulletRadius;
|
|
if (radius <= 0) {
|
|
return;
|
|
}
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(markdown.list.bulletFg->c);
|
|
p.drawEllipse(QPointF(block.markerCenter), radius, radius);
|
|
}
|
|
|
|
[[nodiscard]] const QImage &ColorizedDisplayFormulaImage(
|
|
const LaidOutBlock &block,
|
|
const RenderedFormula &formula,
|
|
QColor color) {
|
|
const auto size = formula.image.size();
|
|
if (block.colorizedFormulaImage.isNull()
|
|
|| (block.colorizedFormulaSize != size)
|
|
|| (block.colorizedFormulaColor != color)) {
|
|
block.colorizedFormulaImage = QImage(
|
|
size,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
style::colorizeImage(
|
|
formula.image,
|
|
color,
|
|
&block.colorizedFormulaImage,
|
|
QRect(),
|
|
QPoint(),
|
|
true);
|
|
block.colorizedFormulaColor = color;
|
|
block.colorizedFormulaSize = size;
|
|
}
|
|
return block.colorizedFormulaImage;
|
|
}
|
|
|
|
void PaintTableBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto tableClip = clip.intersected(block.visibleTableRect);
|
|
if (tableClip.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
p.save();
|
|
p.setClipRect(tableClip);
|
|
|
|
const auto &table = st::defaultTable;
|
|
const auto half = table.border / 2.;
|
|
const auto inner = QRectF(block.tableRect).marginsRemoved(
|
|
{ half, half, half, half });
|
|
const auto radius = table.radius;
|
|
auto outerPath = QPainterPath();
|
|
outerPath.addRoundedRect(inner, radius, radius);
|
|
|
|
auto headerBottom = 0;
|
|
for (const auto &row : block.tableRows) {
|
|
if (!row.header) {
|
|
break;
|
|
}
|
|
headerBottom = row.outer.y() + row.outer.height();
|
|
}
|
|
if (headerBottom > 0) {
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setClipRect(
|
|
QRect(
|
|
block.tableRect.x(),
|
|
block.tableRows.front().outer.y(),
|
|
block.tableRect.width(),
|
|
headerBottom - block.tableRows.front().outer.y()),
|
|
Qt::IntersectClip);
|
|
p.setBrush(table.headerBg);
|
|
p.setPen(Qt::NoPen);
|
|
p.drawRoundedRect(inner, radius, radius);
|
|
p.setClipping(false);
|
|
p.setClipRect(tableClip);
|
|
}
|
|
|
|
if (table.border > 0 && !block.tableRect.isEmpty()) {
|
|
auto path = outerPath;
|
|
for (auto i = 1, count = int(block.tableRows.size()); i != count; ++i) {
|
|
const auto y = block.tableRows[i].outer.y() - half;
|
|
path.moveTo(inner.x(), y);
|
|
path.lineTo(inner.x() + inner.width(), y);
|
|
}
|
|
|
|
auto separatorLeft = block.tableRect.x() + table.border;
|
|
for (auto i = 0, count = int(block.tableColumnWidths.size()); i + 1 < count; ++i) {
|
|
separatorLeft += block.tableColumnWidths[i];
|
|
const auto x = separatorLeft + half;
|
|
path.moveTo(x, inner.y());
|
|
path.lineTo(x, inner.y() + inner.height());
|
|
separatorLeft += table.border;
|
|
}
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
auto pen = table.borderFg->p;
|
|
pen.setWidth(table.border);
|
|
p.setPen(pen);
|
|
p.setBrush(Qt::NoBrush);
|
|
p.drawPath(path);
|
|
}
|
|
|
|
p.setPen(markdown.textColor->c);
|
|
for (const auto &row : block.tableRows) {
|
|
if (!row.outer.intersects(block.visibleTableRect)) {
|
|
continue;
|
|
}
|
|
for (const auto &cell : row.cells) {
|
|
if (!cell.textRect.intersects(block.visibleTableRect)) {
|
|
continue;
|
|
}
|
|
PaintTextLeaf(
|
|
p,
|
|
cell.leaf,
|
|
caches,
|
|
cell.textRect,
|
|
cell.textWidth,
|
|
tableClip,
|
|
cell.align,
|
|
TextSelectionForSegmentIndex(
|
|
selectionState,
|
|
cell.segmentIndex));
|
|
}
|
|
}
|
|
|
|
if (block.segmentIndex >= 0
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
|
p.save();
|
|
p.setClipPath(outerPath, Qt::IntersectClip);
|
|
p.fillRect(block.tableRect, p.textPalette().selectOverlay);
|
|
p.restore();
|
|
}
|
|
|
|
if (block.overflowed) {
|
|
const auto indicatorWidth = std::min(
|
|
std::max(markdown.table.overflowWidth, 1),
|
|
block.visibleTableRect.width());
|
|
p.save();
|
|
p.setClipPath(outerPath, Qt::IntersectClip);
|
|
p.fillRect(
|
|
QRect(
|
|
block.visibleTableRect.x()
|
|
+ block.visibleTableRect.width()
|
|
- indicatorWidth,
|
|
block.visibleTableRect.y(),
|
|
indicatorWidth,
|
|
block.visibleTableRect.height()),
|
|
markdown.table.overflowFg->c);
|
|
p.restore();
|
|
}
|
|
|
|
p.restore();
|
|
}
|
|
|
|
void PaintDisplayMathBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
int devicePixelRatio,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto formulaClip = clip.intersected(block.visibleFormulaRect);
|
|
if (formulaClip.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
p.save();
|
|
p.setClipRect(formulaClip);
|
|
|
|
const auto formula = PreparedFormulaFor(formulas, block.formulaIndex);
|
|
p.setPen(markdown.textColor->c);
|
|
const auto rendered = EnsureFormulaRendered(
|
|
formula,
|
|
FormulaRasterSlot(renderedFormulas, block.formulaIndex),
|
|
renderer,
|
|
devicePixelRatio);
|
|
if (rendered.success) {
|
|
p.drawImage(
|
|
block.formulaRect.topLeft(),
|
|
ColorizedDisplayFormulaImage(
|
|
block,
|
|
rendered,
|
|
p.pen().color()));
|
|
}
|
|
if (!rendered.success) {
|
|
const auto radius = markdown.displayMath.fallbackRadius;
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(markdown.displayMath.fallbackBg->c);
|
|
if (radius > 0) {
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.drawRoundedRect(block.formulaRect, radius, radius);
|
|
} else {
|
|
p.fillRect(block.formulaRect, markdown.displayMath.fallbackBg->c);
|
|
}
|
|
p.setPen(markdown.textColor->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.fallbackLeaf,
|
|
caches,
|
|
block.textRect,
|
|
block.textWidth,
|
|
formulaClip);
|
|
}
|
|
|
|
if (block.segmentIndex >= 0
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
|
p.fillRect(block.visibleFormulaRect, p.textPalette().selectOverlay);
|
|
}
|
|
|
|
if (block.overflowed) {
|
|
const auto indicatorWidth = std::min(
|
|
std::max(markdown.displayMath.overflowWidth, 1),
|
|
block.visibleFormulaRect.width());
|
|
p.fillRect(
|
|
QRect(
|
|
block.visibleFormulaRect.x()
|
|
+ block.visibleFormulaRect.width()
|
|
- indicatorWidth,
|
|
block.visibleFormulaRect.y(),
|
|
indicatorWidth,
|
|
block.visibleFormulaRect.height()),
|
|
markdown.displayMath.overflowFg->c);
|
|
}
|
|
|
|
p.restore();
|
|
}
|
|
|
|
void PaintQuoteBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
int devicePixelRatio,
|
|
int outerWidth,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto quoteClip = clip.intersected(block.outer);
|
|
if (quoteClip.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (caches.blockquote) {
|
|
const auto "eStyle = markdown.body.blockquote;
|
|
Ui::Text::ValidateQuotePaintCache(*caches.blockquote, quoteStyle);
|
|
|
|
p.save();
|
|
p.setClipRect(quoteClip);
|
|
Ui::Text::FillQuotePaint(
|
|
p,
|
|
block.outer,
|
|
*caches.blockquote,
|
|
quoteStyle);
|
|
p.restore();
|
|
}
|
|
|
|
PaintBlocks(
|
|
p,
|
|
block.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip.intersected(block.contentRect));
|
|
}
|
|
|
|
void PaintPlaceholderBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto visible = clip.intersected(block.visibleMediaRect);
|
|
if (!visible.isEmpty()) {
|
|
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.segmentIndex >= 0
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
|
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
|
|
}
|
|
p.restore();
|
|
}
|
|
if (!block.textRect.isEmpty()) {
|
|
p.setPen(markdown.textColor->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.leaf,
|
|
caches,
|
|
block.textRect,
|
|
block.textWidth,
|
|
clip,
|
|
style::al_left,
|
|
TextSelectionForSegmentIndex(
|
|
selectionState,
|
|
block.secondarySegmentIndex));
|
|
}
|
|
}
|
|
|
|
void PaintPhotoProgress(
|
|
Painter &p,
|
|
QRect rect,
|
|
const style::MarkdownPhoto &style,
|
|
double progress) {
|
|
const auto size = std::min({
|
|
style.progressSize,
|
|
rect.width(),
|
|
rect.height(),
|
|
});
|
|
if (size <= 0) {
|
|
return;
|
|
}
|
|
const auto thickness = std::max(style.progressWidth, 1);
|
|
const auto ring = QRect(
|
|
rect.center().x() - (size / 2),
|
|
rect.center().y() - (size / 2),
|
|
size,
|
|
size);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setBrush(Qt::NoBrush);
|
|
p.setPen(QPen(QColor(0, 0, 0, 96), thickness));
|
|
p.drawEllipse(ring);
|
|
p.setPen(QPen(st::windowFg->c, thickness));
|
|
p.drawArc(
|
|
ring,
|
|
90 * 16,
|
|
-int(std::round(360. * 16. * std::clamp(progress, 0., 1.))));
|
|
}
|
|
|
|
[[nodiscard]] QPainterPath RoundedRectPath(QRect rect, int radius) {
|
|
auto path = QPainterPath();
|
|
path.addRoundedRect(QRectF(rect), radius, radius);
|
|
return path;
|
|
}
|
|
|
|
[[nodiscard]] bool MediaLoading(const LaidOutBlock &block) {
|
|
if (block.photoRuntime) {
|
|
return block.photoRuntime->loading();
|
|
} else if (block.documentRuntime) {
|
|
return block.documentRuntime->loading();
|
|
} else if (block.mapRuntime) {
|
|
return block.mapRuntime->loading();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] double MediaProgress(const LaidOutBlock &block) {
|
|
if (block.photoRuntime) {
|
|
return block.photoRuntime->progress();
|
|
} else if (block.documentRuntime) {
|
|
return block.documentRuntime->progress();
|
|
} else if (block.mapRuntime) {
|
|
return block.mapRuntime->progress();
|
|
}
|
|
return 0.;
|
|
}
|
|
|
|
[[nodiscard]] bool MediaLoading(const LaidOutGroupedMediaItem &item) {
|
|
if (item.photoRuntime) {
|
|
return item.photoRuntime->loading();
|
|
} else if (item.documentRuntime) {
|
|
return item.documentRuntime->loading();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] double MediaProgress(const LaidOutGroupedMediaItem &item) {
|
|
if (item.photoRuntime) {
|
|
return item.photoRuntime->progress();
|
|
} else if (item.documentRuntime) {
|
|
return item.documentRuntime->progress();
|
|
}
|
|
return 0.;
|
|
}
|
|
|
|
void PaintMediaCaption(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
if (block.textRect.isEmpty()) {
|
|
return;
|
|
}
|
|
p.setPen(markdown.textColor->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.leaf,
|
|
caches,
|
|
block.textRect,
|
|
block.textWidth,
|
|
clip,
|
|
style::al_left,
|
|
TextSelectionForSegmentIndex(
|
|
selectionState,
|
|
block.secondarySegmentIndex));
|
|
}
|
|
|
|
void PaintImageBackedMedia(
|
|
Painter &p,
|
|
QRect rect,
|
|
const QString ©Text,
|
|
const std::shared_ptr<Ui::DynamicImage> &thumbnailImage,
|
|
const std::shared_ptr<Ui::DynamicImage> &fullImage,
|
|
bool *thumbnailSubscribed,
|
|
bool *fullSubscribed,
|
|
bool loading,
|
|
double progress,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
bool selected,
|
|
QRect clip) {
|
|
const auto visible = clip.intersected(rect);
|
|
if (!visible.isEmpty()) {
|
|
p.save();
|
|
p.setClipRect(visible);
|
|
p.fillRect(rect, st::windowBgOver->c);
|
|
SubscribeDynamicImage(
|
|
thumbnailImage,
|
|
caches.repaint,
|
|
thumbnailSubscribed);
|
|
SubscribeDynamicImage(
|
|
fullImage,
|
|
caches.repaint,
|
|
fullSubscribed);
|
|
const auto paintedThumb = PaintDynamicImage(
|
|
p,
|
|
thumbnailImage,
|
|
rect);
|
|
const auto paintedFull = PaintDynamicImage(
|
|
p,
|
|
fullImage,
|
|
rect);
|
|
if (!paintedThumb && !paintedFull) {
|
|
p.setPen(st::windowSubTextFg->c);
|
|
p.drawText(
|
|
rect,
|
|
Qt::AlignCenter | Qt::TextWordWrap,
|
|
copyText);
|
|
}
|
|
if (loading) {
|
|
PaintPhotoProgress(
|
|
p,
|
|
rect,
|
|
markdown.photo,
|
|
progress);
|
|
}
|
|
if (selected) {
|
|
p.fillRect(rect, p.textPalette().selectOverlay);
|
|
}
|
|
p.restore();
|
|
}
|
|
}
|
|
|
|
void PaintImageBackedMediaBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
PaintImageBackedMedia(
|
|
p,
|
|
block.mediaRect,
|
|
block.copyText,
|
|
block.thumbnailImage,
|
|
block.fullImage,
|
|
&block.thumbnailSubscribed,
|
|
&block.fullSubscribed,
|
|
MediaLoading(block),
|
|
MediaProgress(block),
|
|
markdown,
|
|
caches,
|
|
block.segmentIndex >= 0
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex),
|
|
clip);
|
|
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
|
}
|
|
|
|
void PaintCardSurface(
|
|
Painter &p,
|
|
QRect rect,
|
|
int border,
|
|
const style::color &borderFg,
|
|
const style::color &bg,
|
|
int radius) {
|
|
if (rect.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto half = border / 2.;
|
|
const auto inner = QRectF(rect).marginsRemoved({
|
|
half,
|
|
half,
|
|
half,
|
|
half,
|
|
});
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(QPen(borderFg->c, border));
|
|
p.setBrush(bg->c);
|
|
p.drawRoundedRect(inner, radius, radius);
|
|
}
|
|
|
|
void PaintAudioBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto visible = clip.intersected(block.visibleMediaRect);
|
|
if (!visible.isEmpty()) {
|
|
const auto &style = markdown.audio;
|
|
p.save();
|
|
p.setClipRect(visible);
|
|
PaintCardSurface(
|
|
p,
|
|
block.mediaRect,
|
|
style.border,
|
|
style.borderFg,
|
|
style.bg,
|
|
style.radius);
|
|
p.setPen(style.titleFg->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.labelLeaf,
|
|
caches,
|
|
block.labelRect,
|
|
block.labelWidth,
|
|
visible);
|
|
if (!block.subtitleRect.isEmpty()) {
|
|
p.setPen(style.subtitleFg->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.subtitleLeaf,
|
|
caches,
|
|
block.subtitleRect,
|
|
block.subtitleWidth,
|
|
visible);
|
|
}
|
|
if (block.segmentIndex >= 0
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
|
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
|
|
}
|
|
p.restore();
|
|
}
|
|
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
|
}
|
|
|
|
void PaintChannelBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto visible = clip.intersected(block.visibleMediaRect);
|
|
if (!visible.isEmpty()) {
|
|
const auto &style = markdown.channel;
|
|
const auto &button = style.button;
|
|
p.save();
|
|
p.setClipRect(visible);
|
|
PaintCardSurface(
|
|
p,
|
|
block.mediaRect,
|
|
style.border,
|
|
style.borderFg,
|
|
style.bg,
|
|
style.radius);
|
|
p.setPen(style.titleFg->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.labelLeaf,
|
|
caches,
|
|
block.labelRect,
|
|
block.labelWidth,
|
|
visible);
|
|
if (!block.subtitleRect.isEmpty()) {
|
|
p.setPen(style.subtitleFg->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.subtitleLeaf,
|
|
caches,
|
|
block.subtitleRect,
|
|
block.subtitleWidth,
|
|
visible);
|
|
}
|
|
if (block.channelRuntime
|
|
&& block.channelRuntime->joinVisible()
|
|
&& !block.actionRect.isEmpty()) {
|
|
const auto innerRect = block.actionRect.marginsRemoved(button.padding);
|
|
const auto half = button.border / 2.;
|
|
const auto outer = QRectF(block.actionRect).marginsRemoved({
|
|
half,
|
|
half,
|
|
half,
|
|
half,
|
|
});
|
|
{
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(QPen(button.borderFg->c, button.border));
|
|
p.setBrush(button.bg->c);
|
|
p.drawRoundedRect(outer, button.radius, button.radius);
|
|
}
|
|
p.setPen(button.textFg->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.actionLeaf,
|
|
caches,
|
|
innerRect,
|
|
block.actionWidth,
|
|
visible,
|
|
style::al_center);
|
|
}
|
|
if (block.segmentIndex >= 0
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
|
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
|
|
}
|
|
p.restore();
|
|
}
|
|
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
|
}
|
|
|
|
void PaintPhotoBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
PaintImageBackedMediaBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
}
|
|
|
|
void PaintVideoBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
PaintImageBackedMediaBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
}
|
|
|
|
void PaintMapBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
PaintImageBackedMediaBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
}
|
|
|
|
void PaintGroupedMediaBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto selected = (block.segmentIndex >= 0)
|
|
&& WholeSegmentSelected(selectionState, block.segmentIndex);
|
|
const auto visible = clip.intersected(block.visibleMediaRect);
|
|
if (!visible.isEmpty()) {
|
|
const auto &style = markdown.groupedMedia;
|
|
p.save();
|
|
p.setClipRect(visible);
|
|
const auto path = RoundedRectPath(block.mediaRect, style.radius);
|
|
p.setClipPath(path, Qt::IntersectClip);
|
|
for (const auto &item : block.groupedMediaItems) {
|
|
PaintImageBackedMedia(
|
|
p,
|
|
item.rect,
|
|
item.copyText,
|
|
item.thumbnailImage,
|
|
item.fullImage,
|
|
&item.thumbnailSubscribed,
|
|
&item.fullSubscribed,
|
|
MediaLoading(item),
|
|
MediaProgress(item),
|
|
markdown,
|
|
caches,
|
|
false,
|
|
clip);
|
|
}
|
|
if (selected) {
|
|
auto overlay = p.textPalette().selectOverlay->c;
|
|
overlay.setAlphaF(std::clamp(style.overlayOpacity, 0., 1.));
|
|
p.fillPath(path, overlay);
|
|
}
|
|
p.restore();
|
|
}
|
|
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
|
}
|
|
|
|
void PaintDetailsBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
int devicePixelRatio,
|
|
int outerWidth,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
const auto visible = clip.intersected(block.outer);
|
|
if (visible.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const auto &details = markdown.details;
|
|
const auto half = details.border / 2.;
|
|
const auto outer = QRectF(block.outer).marginsRemoved({
|
|
half,
|
|
half,
|
|
half,
|
|
half,
|
|
});
|
|
auto outerPath = QPainterPath();
|
|
outerPath.addRoundedRect(outer, details.radius, details.radius);
|
|
|
|
p.save();
|
|
p.setClipRect(visible);
|
|
{
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.fillPath(
|
|
outerPath,
|
|
(block.bodyRect.isEmpty() ? details.headerBg : details.bodyBg)->c);
|
|
p.save();
|
|
p.setClipPath(outerPath, Qt::IntersectClip);
|
|
p.fillRect(block.headerRect, details.headerBg->c);
|
|
if (!block.bodyRect.isEmpty()) {
|
|
p.setPen(QPen(details.borderFg->c, details.border));
|
|
const auto separatorY = block.bodyRect.top() + half;
|
|
p.drawLine(
|
|
QPointF(outer.left(), separatorY),
|
|
QPointF(outer.right(), separatorY));
|
|
}
|
|
p.restore();
|
|
|
|
p.setBrush(Qt::NoBrush);
|
|
p.setPen(QPen(details.borderFg->c, details.border));
|
|
p.drawPath(outerPath);
|
|
}
|
|
if (!block.iconRect.isEmpty()) {
|
|
details.icon.paint(
|
|
p,
|
|
block.iconRect.x(),
|
|
block.iconRect.y(),
|
|
outerWidth);
|
|
}
|
|
p.setPen(details.summaryFg->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.leaf,
|
|
caches,
|
|
block.textRect,
|
|
block.textWidth,
|
|
clip,
|
|
style::al_left,
|
|
TextSelectionForSegmentIndex(
|
|
selectionState,
|
|
block.segmentIndex));
|
|
p.restore();
|
|
|
|
if (!block.bodyRect.isEmpty()) {
|
|
PaintBlocks(
|
|
p,
|
|
block.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip.intersected(block.bodyRect));
|
|
}
|
|
}
|
|
|
|
void PaintBlock(
|
|
Painter &p,
|
|
const LaidOutBlock &block,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
int devicePixelRatio,
|
|
int outerWidth,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
if (!block.outer.intersects(clip)) {
|
|
return;
|
|
}
|
|
|
|
switch (block.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Heading:
|
|
p.setPen(markdown.textColor->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.leaf,
|
|
caches,
|
|
block.textRect,
|
|
block.textWidth,
|
|
clip,
|
|
style::al_left,
|
|
TextSelectionForSegmentIndex(
|
|
selectionState,
|
|
block.segmentIndex));
|
|
break;
|
|
case PreparedBlockKind::CodeBlock:
|
|
p.setPen(markdown.textColor->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.leaf,
|
|
caches,
|
|
block.textRect,
|
|
block.textWidth,
|
|
clip,
|
|
style::al_left,
|
|
TextSelectionForSegmentIndex(
|
|
selectionState,
|
|
block.segmentIndex));
|
|
break;
|
|
case PreparedBlockKind::Rule:
|
|
p.fillRect(block.outer, st::defaultTable.borderFg->c);
|
|
break;
|
|
case PreparedBlockKind::List:
|
|
PaintBlocks(
|
|
p,
|
|
block.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::ListItem:
|
|
if (block.taskState != TaskState::None) {
|
|
PaintTaskMarker(p, block, markdown, outerWidth);
|
|
} else if (block.listKind == ListKind::Ordered
|
|
&& !block.markerRect.isEmpty()) {
|
|
p.setPen(markdown.textColor->c);
|
|
PaintTextLeaf(
|
|
p,
|
|
block.marker,
|
|
caches,
|
|
block.markerRect,
|
|
block.markerWidth,
|
|
clip);
|
|
} else if (block.listKind == ListKind::Bullet) {
|
|
PaintBulletMarker(p, block, markdown);
|
|
}
|
|
PaintBlocks(
|
|
p,
|
|
block.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Quote:
|
|
PaintQuoteBlock(
|
|
p,
|
|
block,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::DisplayMath:
|
|
PaintDisplayMathBlock(
|
|
p,
|
|
block,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Table:
|
|
PaintTableBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Photo:
|
|
PaintPhotoBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Video:
|
|
PaintVideoBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Audio:
|
|
PaintAudioBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Map:
|
|
PaintMapBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Channel:
|
|
PaintChannelBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Placeholder:
|
|
PaintPlaceholderBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::GroupedMedia:
|
|
PaintGroupedMediaBlock(
|
|
p,
|
|
block,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
case PreparedBlockKind::Details:
|
|
PaintDetailsBlock(
|
|
p,
|
|
block,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void PaintBlocks(
|
|
Painter &p,
|
|
const std::vector<LaidOutBlock> &blocks,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
int devicePixelRatio,
|
|
int outerWidth,
|
|
const style::Markdown &markdown,
|
|
const MarkdownArticlePaintCaches &caches,
|
|
const PaintSelectionState &selectionState,
|
|
QRect clip) {
|
|
for (const auto &block : blocks) {
|
|
if (block.outer.bottom() < clip.top()) {
|
|
continue;
|
|
} else if (block.outer.top() > clip.bottom()) {
|
|
break;
|
|
}
|
|
PaintBlock(
|
|
p,
|
|
block,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
devicePixelRatio,
|
|
outerWidth,
|
|
markdown,
|
|
caches,
|
|
selectionState,
|
|
clip);
|
|
}
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|