Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp
T
2026-05-21 22:07:11 +04:00

1531 lines
37 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/dynamic_image.h"
#include "ui/widgets/checkbox.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]] QRectF CenterCropSourceRect(QSize source, QSize target) {
const auto sourceWidth = std::max(source.width(), 1);
const auto sourceHeight = std::max(source.height(), 1);
const auto targetWidth = std::max(target.width(), 1);
const auto targetHeight = std::max(target.height(), 1);
const auto sourceRatio = float64(sourceWidth) / sourceHeight;
const auto targetRatio = float64(targetWidth) / targetHeight;
if (sourceRatio > targetRatio) {
const auto width = sourceHeight * targetRatio;
return QRectF(
(sourceWidth - width) / 2.,
0.,
width,
sourceHeight);
}
const auto height = sourceWidth / targetRatio;
return QRectF(
0.,
(sourceHeight - height) / 2.,
sourceWidth,
height);
}
void PaintImageCenterCrop(Painter &p, QRect rect, const QImage &image) {
p.drawImage(
QRectF(rect),
image,
CenterCropSourceRect(image.size(), rect.size()));
}
[[nodiscard]] bool ImageCoversRect(const QImage &image, QRect rect) {
const auto ratio = std::max(image.devicePixelRatio(), 1.);
return (image.width() / ratio >= rect.width())
&& (image.height() / ratio >= rect.height());
}
[[nodiscard]] bool PaintDynamicImage(
Painter &p,
const std::shared_ptr<Ui::DynamicImage> &image,
QRect rect,
bool requireCovering = false) {
if (!image || rect.isEmpty()) {
return false;
}
if (const auto frame = image->image(std::max(rect.width(), rect.height()));
!frame.isNull()) {
if (requireCovering && !ImageCoversRect(frame, rect)) {
return false;
}
PaintImageCenterCrop(p, rect, frame);
return true;
}
return false;
}
[[nodiscard]] bool PaintThumbnailImage(
Painter &p,
QRect rect,
const std::shared_ptr<Ui::DynamicImage> &thumbnail,
const std::shared_ptr<Ui::DynamicImage> &previousThumbnail) {
return PaintDynamicImage(p, thumbnail, rect, true)
|| PaintDynamicImage(p, previousThumbnail, rect, true)
|| PaintDynamicImage(p, previousThumbnail, rect)
|| PaintDynamicImage(p, thumbnail, rect);
}
void RefreshBlockThumbnail(
const LaidOutBlock &block,
const MarkdownArticlePaintCaches &caches) {
if (!block.photoRuntime || block.thumbnailRect.isEmpty()) {
return;
}
const auto size = block.thumbnailRect.size();
if (size.isEmpty() || block.thumbnailRequestSize == size) {
return;
}
block.thumbnailRequestSize = size;
if (const auto image = block.photoRuntime->thumbnail(size)) {
if (image != block.thumbnailImage) {
block.previousThumbnailImage = std::move(block.thumbnailImage);
block.thumbnailImage = image;
}
if (image != block.subscribedThumbnailImage) {
block.subscribedThumbnailImage = image;
const auto repaint = caches.repaint;
const auto repaintRect = caches.repaintRect;
const auto rect = block.mediaRect;
image->subscribeToUpdates([repaint, repaintRect, rect] {
if (repaintRect && !rect.isEmpty()) {
repaintRect(rect);
} else if (repaint) {
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,
int elisionLines = 0) {
const auto availableWidth = std::max(width, 1);
leaf.draw(p, {
.position = rect.topLeft(),
.availableWidth = availableWidth,
.geometry = elisionLines
? Ui::Text::SimpleGeometry(availableWidth, elisionLines, 0, true)
: 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()),
.elisionLines = elisionLines,
});
}
[[nodiscard]] std::optional<QColor> QuoteSupplementaryColor(
const MarkdownArticlePaintCaches &caches) {
if (!caches.blockquote) {
return {};
}
return anim::color(
caches.blockquote->bg,
caches.blockquote->icon,
0.75);
}
void SetTextLeafPen(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches) {
p.setPen(!block.supplementary
? markdown.textColor->c
: caches.supplementaryColorOverride.value_or(
markdown.supplementaryTextColor->c));
}
void PaintRelatedArticleTextLeaf(
Painter &p,
const Ui::Text::String &leaf,
const MarkdownArticlePaintCaches &caches,
QRect rect,
int width,
QRect clip,
int elisionLines) {
const auto textClip = clip.intersected(rect);
if (textClip.isEmpty()) {
return;
}
p.save();
p.setClipRect(textClip, Qt::IntersectClip);
PaintTextLeaf(
p,
leaf,
caches,
rect,
width,
textClip,
style::al_left,
std::nullopt,
elisionLines);
p.restore();
}
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;
}
[[nodiscard]] int TableBorder(
const LaidOutBlock &block,
const style::Markdown &markdown) {
return block.tableBordered ? markdown.table.border : 0;
}
struct TableOwnershipSlot {
const LaidOutTableCell *cell = nullptr;
};
using TableOwnershipGrid = std::vector<std::vector<TableOwnershipSlot>>;
[[nodiscard]] TableOwnershipGrid BuildTableOwnershipGrid(
const LaidOutBlock &block) {
const auto rowCount = int(block.tableRows.size());
const auto columnCount = int(block.tableColumnWidths.size());
auto result = TableOwnershipGrid(
std::max(rowCount, 0),
std::vector<TableOwnershipSlot>(std::max(columnCount, 0)));
for (auto row = 0; row != rowCount; ++row) {
for (const auto &cell : block.tableRows[row].cells) {
const auto fromRow = std::clamp(row, 0, rowCount);
const auto toRow = std::clamp(row + cell.rowspan, 0, rowCount);
const auto fromColumn = std::clamp(cell.column, 0, columnCount);
const auto toColumn = std::clamp(
cell.column + cell.colspan,
0,
columnCount);
for (auto currentRow = fromRow; currentRow != toRow; ++currentRow) {
for (auto currentColumn = fromColumn;
currentColumn != toColumn;
++currentColumn) {
result[currentRow][currentColumn].cell = &cell;
}
}
}
}
return result;
}
[[nodiscard]] QPainterPath TableShapePath(
const LaidOutBlock &block,
int border,
int radius) {
auto path = QPainterPath();
if (block.tableRect.isEmpty()) {
return path;
}
if (border > 0) {
const auto half = border / 2.;
path.addRoundedRect(
QRectF(block.tableRect).marginsRemoved({
half,
half,
half,
half,
}),
radius,
radius);
} else {
path.addRect(block.tableRect);
}
return path;
}
[[nodiscard]] auto TableColumnLefts(
const LaidOutBlock &block,
int columnCount,
int border) -> std::vector<int> {
auto result = std::vector<int>(
std::max(columnCount, 0),
block.tableRect.x() + border);
auto separatorLeft = block.tableRect.x() + border;
for (auto column = 0; column != columnCount; ++column) {
result[column] = separatorLeft;
if (column < int(block.tableColumnWidths.size())) {
separatorLeft += block.tableColumnWidths[column] + border;
}
}
return result;
}
void AddTableHorizontalBorderSegments(
QPainterPath *path,
const LaidOutBlock &block,
const TableOwnershipGrid &ownership,
const std::vector<int> &columnLefts,
QRectF inner,
float64 half) {
const auto rowCount = int(ownership.size());
const auto columnCount = rowCount ? int(ownership.front().size()) : 0;
for (auto boundaryRow = 1; boundaryRow != rowCount; ++boundaryRow) {
auto segmentStart = -1;
for (auto column = 0; column != columnCount; ++column) {
const auto split = ownership[boundaryRow - 1][column].cell
!= ownership[boundaryRow][column].cell;
if (split && (segmentStart < 0)) {
segmentStart = column;
} else if (!split && (segmentStart >= 0)) {
const auto fromX = (segmentStart == 0)
? inner.x()
: (columnLefts[segmentStart] - half);
const auto toX = columnLefts[column] - half;
const auto y = block.tableRows[boundaryRow].outer.y() - half;
path->moveTo(fromX, y);
path->lineTo(toX, y);
segmentStart = -1;
}
}
if (segmentStart >= 0) {
const auto fromX = (segmentStart == 0)
? inner.x()
: (columnLefts[segmentStart] - half);
const auto y = block.tableRows[boundaryRow].outer.y() - half;
path->moveTo(fromX, y);
path->lineTo(inner.x() + inner.width(), y);
}
}
}
void AddTableVerticalBorderSegments(
QPainterPath *path,
const LaidOutBlock &block,
const TableOwnershipGrid &ownership,
const std::vector<int> &columnLefts,
QRectF inner,
float64 half) {
const auto rowCount = int(ownership.size());
const auto columnCount = rowCount ? int(ownership.front().size()) : 0;
for (auto boundaryColumn = 1;
boundaryColumn != columnCount;
++boundaryColumn) {
auto segmentStart = -1;
for (auto row = 0; row != rowCount; ++row) {
const auto split = ownership[row][boundaryColumn - 1].cell
!= ownership[row][boundaryColumn].cell;
if (split && (segmentStart < 0)) {
segmentStart = row;
} else if (!split && (segmentStart >= 0)) {
const auto fromY = (segmentStart == 0)
? inner.y()
: (block.tableRows[segmentStart].outer.y() - half);
const auto toY = block.tableRows[row].outer.y() - half;
const auto x = columnLefts[boundaryColumn] - half;
path->moveTo(x, fromY);
path->lineTo(x, toY);
segmentStart = -1;
}
}
if (segmentStart >= 0) {
const auto fromY = (segmentStart == 0)
? inner.y()
: (block.tableRows[segmentStart].outer.y() - half);
const auto x = columnLefts[boundaryColumn] - half;
path->moveTo(x, fromY);
path->lineTo(x, inner.y() + inner.height());
}
}
}
[[nodiscard]] QPainterPath TableBorderPath(
const LaidOutBlock &block,
int border,
QPainterPath path) {
const auto ownership = BuildTableOwnershipGrid(block);
const auto rowCount = int(ownership.size());
const auto columnCount = rowCount
? int(ownership.front().size())
: int(block.tableColumnWidths.size());
if (!rowCount || !columnCount) {
return path;
}
const auto half = border / 2.;
const auto columnLefts = TableColumnLefts(block, columnCount, border);
const auto inner = QRectF(block.tableRect).marginsRemoved({
half,
half,
half,
half,
});
AddTableHorizontalBorderSegments(
&path,
block,
ownership,
columnLefts,
inner,
half);
AddTableVerticalBorderSegments(
&path,
block,
ownership,
columnLefts,
inner,
half);
return path;
}
void PaintTableBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
if (!block.textRect.isEmpty()) {
SetTextLeafPen(p, block, markdown, caches);
PaintTextLeaf(
p,
block.leaf,
caches,
block.textRect,
block.textWidth,
clip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.secondarySegmentIndex));
}
const auto tableClip = clip.intersected(block.visibleTableRect);
if (tableClip.isEmpty()) {
return;
}
const auto border = TableBorder(block, markdown);
const auto radius = st::defaultTable.radius;
const auto shapePath = TableShapePath(block, border, radius);
p.save();
p.setClipRect(tableClip);
p.save();
p.setClipPath(shapePath, Qt::IntersectClip);
for (auto rowIndex = 0, rowCount = int(block.tableRows.size()); rowIndex != rowCount; ++rowIndex) {
const auto striped = block.tableStriped && ((rowIndex % 2) == 0);
for (const auto &cell : block.tableRows[rowIndex].cells) {
if (!cell.outer.intersects(block.visibleTableRect)) {
continue;
}
if (!cell.header && !striped) {
continue;
}
p.fillRect(cell.outer, markdown.table.headerBg->c);
}
}
p.restore();
if (border > 0 && !block.tableRect.isEmpty()) {
const auto path = TableBorderPath(block, border, shapePath);
auto hq = PainterHighQualityEnabler(p);
auto pen = markdown.table.borderFg->p;
pen.setWidth(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(shapePath, 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(shapePath, 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 &quoteStyle = 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();
}
auto overriden = caches;
overriden.supplementaryColorOverride = QuoteSupplementaryColor(caches);
PaintBlocks(
p,
block.children,
formulas,
renderedFormulas,
renderer,
devicePixelRatio,
outerWidth,
markdown,
overriden,
selectionState,
clip.intersected(block.contentRect));
}
void PaintPlaceholderBlock(
Painter &p,
const LaidOutBlock &block,
int outerWidth,
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);
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);
}
p.restore();
}
if (!block.textRect.isEmpty()) {
SetTextLeafPen(p, block, markdown, caches);
PaintTextLeaf(
p,
block.leaf,
caches,
block.textRect,
block.textWidth,
clip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.secondarySegmentIndex));
}
}
[[nodiscard]] QPainterPath RoundedRectPath(QRect rect, int radius) {
auto path = QPainterPath();
path.addRoundedRect(QRectF(rect), radius, radius);
return path;
}
void PaintEmbedPostBlock(
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 mediaClip = clip.intersected(block.mediaRect);
const auto &style = markdown.embedPost;
if (!mediaClip.isEmpty()) {
RefreshBlockThumbnail(block, caches);
p.save();
p.setClipRect(mediaClip);
if (style.accentWidth > 0) {
p.fillRect(
QRect(
block.mediaRect.x(),
block.mediaRect.y(),
style.accentWidth,
block.mediaRect.height()),
style.accentFg->c);
}
if (block.photoRuntime && !block.thumbnailRect.isEmpty()) {
auto hq = PainterHighQualityEnabler(p);
const auto avatarPath = RoundedRectPath(
block.thumbnailRect,
style.avatarRadius);
p.fillPath(avatarPath, st::windowBg->c);
p.save();
p.setClipPath(
avatarPath,
Qt::IntersectClip);
(void)PaintThumbnailImage(
p,
block.thumbnailRect,
block.thumbnailImage,
block.previousThumbnailImage);
p.restore();
}
if (!block.labelRect.isEmpty()) {
p.setPen(style.authorFg->c);
PaintTextLeaf(
p,
block.labelLeaf,
caches,
block.labelRect,
block.labelWidth,
mediaClip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.segmentIndex));
}
if (!block.subtitleRect.isEmpty()) {
p.setPen(style.dateFg->c);
PaintTextLeaf(
p,
block.subtitleLeaf,
caches,
block.subtitleRect,
block.subtitleWidth,
mediaClip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.secondarySegmentIndex));
}
p.restore();
}
if (!block.bodyRect.isEmpty()) {
PaintBlocks(
p,
block.children,
formulas,
renderedFormulas,
renderer,
devicePixelRatio,
outerWidth,
markdown,
caches,
selectionState,
clip.intersected(block.bodyRect));
}
if (!block.textRect.isEmpty()) {
SetTextLeafPen(p, block, markdown, caches);
PaintTextLeaf(
p,
block.leaf,
caches,
block.textRect,
block.textWidth,
clip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.tertiarySegmentIndex));
}
}
void PaintMediaCaption(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
if (block.textRect.isEmpty()) {
return;
}
SetTextLeafPen(p, block, markdown, caches);
PaintTextLeaf(
p,
block.leaf,
caches,
block.textRect,
block.textWidth,
clip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.secondarySegmentIndex));
}
void PaintPersistentMediaBlock(
Painter &p,
const LaidOutBlock &block,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
if (!block.mediaBlock) {
return;
}
block.mediaBlock->paint(p, clip, caches);
if (block.segmentIndex >= 0
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
const auto visible = clip.intersected(block.visibleMediaRect);
if (!visible.isEmpty()) {
p.save();
p.setClipRect(visible);
p.fillRect(block.mediaRect, p.textPalette().selectOverlay);
p.restore();
}
}
}
void PaintCardSurface(
Painter &p,
QRect rect,
int border,
const style::color &borderFg,
const style::color &bg,
int radius) {
if (rect.isEmpty()) {
return;
}
if (border <= 0) {
p.fillRect(rect, bg->c);
return;
}
const auto half = border / 2.;
const auto inner = QRectF(rect).marginsRemoved({
half,
half,
half,
half,
});
if (radius > 0) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen(borderFg->c, border));
p.setBrush(bg->c);
p.drawRoundedRect(inner, radius, radius);
} else {
p.fillRect(rect, bg->c);
p.setPen(QPen(borderFg->c, border));
p.setBrush(Qt::NoBrush);
p.drawRect(inner);
}
}
void PaintAudioBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
PaintPersistentMediaBlock(p, block, caches, selectionState, clip);
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) {
PaintPersistentMediaBlock(p, block, caches, selectionState, clip);
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
}
void PaintRelatedArticleBlock(
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()) {
return;
}
const auto &style = markdown.relatedArticle;
RefreshBlockThumbnail(block, caches);
p.save();
p.setClipRect(visible);
PaintCardSurface(
p,
block.mediaRect,
style.border,
style.borderFg,
style.bg,
style.radius);
if (!block.thumbnailRect.isEmpty()) {
p.fillRect(block.thumbnailRect, st::windowBg->c);
if (style.thumbnailRadius > 0) {
auto hq = PainterHighQualityEnabler(p);
auto path = RoundedRectPath(block.thumbnailRect, style.thumbnailRadius);
p.save();
p.setClipPath(path, Qt::IntersectClip);
(void)PaintThumbnailImage(
p,
block.thumbnailRect,
block.thumbnailImage,
block.previousThumbnailImage);
p.restore();
} else {
(void)PaintThumbnailImage(
p,
block.thumbnailRect,
block.thumbnailImage,
block.previousThumbnailImage);
}
}
if (!block.labelRect.isEmpty()) {
p.setPen(markdown.textColor->c);
PaintRelatedArticleTextLeaf(
p,
block.labelLeaf,
caches,
block.labelRect,
block.labelWidth,
visible,
style.titleLines);
}
if (!block.subtitleRect.isEmpty()) {
p.setPen(markdown.textColor->c);
PaintRelatedArticleTextLeaf(
p,
block.subtitleLeaf,
caches,
block.subtitleRect,
block.subtitleWidth,
visible,
style.subtitleLines);
}
if (!block.actionRect.isEmpty()) {
p.setPen(markdown.supplementaryTextColor->c);
PaintRelatedArticleTextLeaf(
p,
block.actionLeaf,
caches,
block.actionRect,
block.actionWidth,
visible,
style.footerLines);
}
if (block.segmentIndex >= 0
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
}
if (style.separator > 0) {
p.fillRect(
QRect(
block.mediaRect.x(),
block.mediaRect.y() + block.mediaRect.height() - style.separator,
block.mediaRect.width(),
style.separator),
style.separatorFg->c);
}
p.restore();
}
void PaintPhotoBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
PaintPersistentMediaBlock(p, block, caches, selectionState, clip);
PaintMediaCaption(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) {
PaintPersistentMediaBlock(p, block, caches, selectionState, clip);
PaintMediaCaption(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) {
PaintPersistentMediaBlock(p, block, caches, selectionState, clip);
PaintMediaCaption(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) {
if (block.mediaBlock) {
block.mediaBlock->paint(p, clip, caches);
if ((block.segmentIndex >= 0)
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
const auto visible = clip.intersected(block.visibleMediaRect);
if (!visible.isEmpty()) {
const auto &style = markdown.groupedMedia;
auto overlay = p.textPalette().selectOverlay->c;
overlay.setAlphaF(std::clamp(style.overlayOpacity, 0., 1.));
p.save();
p.setClipRect(visible);
p.fillPath(RoundedRectPath(block.mediaRect, style.radius), overlay);
p.restore();
}
}
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
return;
}
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:
if (!block.headerRect.isEmpty()) {
p.fillRect(block.headerRect, markdown.relatedArticle.headerBg->c);
}
SetTextLeafPen(p, block, markdown, caches);
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::RelatedArticle:
PaintRelatedArticleBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::EmbedPost:
PaintEmbedPostBlock(
p,
block,
formulas,
renderedFormulas,
renderer,
devicePixelRatio,
outerWidth,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::Placeholder:
PaintPlaceholderBlock(
p,
block,
outerWidth,
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