/*
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.h"
#include "iv/markdown/iv_markdown_article_text.h"
#include "ui/dynamic_image.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/toggle_arrow.h"
#include "ui/integration.h"
#include "ui/style/style_core_scale.h"
#include "ui/widgets/checkbox.h"

#include "styles/palette.h"
#include "styles/style_iv.h"
#include "styles/style_window.h"
#include "styles/style_widgets.h"

#include <QtGui/QPainterPath>

#include <algorithm>
#include <cmath>
#include <optional>

namespace Iv::Markdown {
namespace {

constexpr auto kThinkingGradientSlideDuration = crl::time(1000);
constexpr auto kThinkingGradientWaitDuration = crl::time(1000);
constexpr auto kThinkingGradientFullDuration = kThinkingGradientSlideDuration
	+ kThinkingGradientWaitDuration;

[[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 UpdateResolvedImage(
		std::shared_ptr<Ui::DynamicImage> *current,
		std::shared_ptr<Ui::DynamicImage> *previous,
		const std::shared_ptr<Ui::DynamicImage> &next) {
	if (!current || !previous || !next) {
		return;
	} else if (next == *current) {
		return;
	} else if (next == *previous) {
		std::swap(*current, *previous);
		return;
	}
	*previous = std::move(*current);
	*current = next;
}

template <typename RequestImage>
void RefreshResolvedBlockImage(
		const LaidOutBlock &block,
		const MarkdownArticlePaintContext &context,
		QSize size,
		QSize *requestedSize,
		std::shared_ptr<Ui::DynamicImage> *current,
		std::shared_ptr<Ui::DynamicImage> *previous,
		std::shared_ptr<Ui::DynamicImage> *subscribed,
		RequestImage &&request) {
	if (size.isEmpty() || !requestedSize || !current || !previous || !subscribed) {
		return;
	} else if (*requestedSize == size) {
		return;
	}
	*requestedSize = size;
	const auto image = request(size);
	if (!image) {
		return;
	}
	UpdateResolvedImage(current, previous, image);
	if (image == *subscribed) {
		return;
	}
	*subscribed = image;
	const auto repaint = context.caches.repaint;
	const auto repaintRect = context.caches.repaintRect;
	const auto rect = block.mediaRect;
	image->subscribeToUpdates([repaint, repaintRect, rect] {
		if (repaintRect && !rect.isEmpty()) {
			repaintRect(rect);
		} else if (repaint) {
			repaint();
		}
	});
}

[[nodiscard]] bool PaintRelatedArticleImage(
		Painter &p,
		QRect rect,
		const std::shared_ptr<Ui::DynamicImage> &thumbnail,
		const std::shared_ptr<Ui::DynamicImage> &full,
		const std::shared_ptr<Ui::DynamicImage> &previousThumbnail,
		const std::shared_ptr<Ui::DynamicImage> &previousFull) {
	return PaintDynamicImage(p, full, rect, true)
		|| PaintDynamicImage(p, previousFull, rect, true)
		|| PaintDynamicImage(p, full, rect)
		|| PaintDynamicImage(p, previousFull, rect)
		|| PaintDynamicImage(p, thumbnail, rect, true)
		|| PaintDynamicImage(p, previousThumbnail, rect, true)
		|| PaintDynamicImage(p, previousThumbnail, rect)
		|| PaintDynamicImage(p, thumbnail, rect);
}

[[nodiscard]] const style::Markdown &PaintStyle(
		const MarkdownArticlePaintContext &context,
		const style::Markdown &st) {
	return context.paintMarkdownStyle(st);
}

[[nodiscard]] MarkdownArticlePaintContext ClippedContext(
		const MarkdownArticlePaintContext &context,
		QRect clip) {
	auto result = context;
	result.clip = clip;
	return result;
}

[[nodiscard]] MarkdownArticlePaintContext RevealSuppressedContext(
		const MarkdownArticlePaintContext &context) {
	auto result = context;
	result.reveal = nullptr;
	return result;
}

[[nodiscard]] std::optional<int> ConsumeRevealLine(
		const MarkdownArticlePaintContext &context) {
	if (!context.reveal) {
		return std::nullopt;
	}
	return context.reveal->nextLine++;
}

[[nodiscard]] int CountGenericRevealBand(QRect rect) {
	return rect.isEmpty() ? 0 : 1;
}

[[nodiscard]] int CountTextRevealLines(
		const Ui::Text::String &leaf,
		QRect textRect,
		int textWidth) {
	if (textRect.isEmpty() || (textWidth <= 0)) {
		return 0;
	}
	return int(leaf.countLinesGeometry(textWidth, true).size());
}

void PaintSelectableTextLeaf(
	Painter &p,
	const Ui::Text::String &leaf,
	const MarkdownArticlePaintContext &context,
	QRect rect,
	int width,
	int segmentIndex,
	style::align align,
	std::optional<TextSelection> selection,
	int elisionLines);

[[nodiscard]] int CountRevealLinesForBlocks(
		const std::vector<LaidOutBlock> &blocks,
		const style::Markdown &st);

[[nodiscard]] int CountTableRevealLines(const LaidOutBlock &block) {
	if (block.visibleTableRect.isEmpty()) {
		return 0;
	}
	auto result = 0;
	for (const auto &row : block.tableRows) {
		if (row.outer.height() > 0) {
			++result;
		}
	}
	return result;
}

[[nodiscard]] const Ui::Text::String &DisplayedEditableLeaf(
		const LaidOutBlock &block) {
	return block.placeholderLeaf.isEmpty()
		? block.leaf
		: block.placeholderLeaf;
}

[[nodiscard]] int CountMediaRevealLines(const LaidOutBlock &block) {
	return CountGenericRevealBand(block.visibleMediaRect)
		+ CountTextRevealLines(
			DisplayedEditableLeaf(block),
			block.textRect,
			block.textWidth);
}

[[nodiscard]] int CountEmbedPostRevealLines(
		const LaidOutBlock &block,
		const style::Markdown &st) {
	auto result = 0;
	if (!block.headerRect.isEmpty()) {
		result += (block.mediaRect.width() > 0) ? 1 : 0;
	} else if (block.children.empty()) {
		result += CountGenericRevealBand(block.mediaRect);
	}
	result += CountRevealLinesForBlocks(block.children, st);
	result += CountTextRevealLines(
		DisplayedEditableLeaf(block),
		block.textRect,
		block.textWidth);
	return result;
}

[[nodiscard]] int CountRevealLinesForBlock(
		const LaidOutBlock &block,
		const style::Markdown &st) {
	switch (block.kind) {
	case PreparedBlockKind::Paragraph:
	case PreparedBlockKind::Thinking:
	case PreparedBlockKind::Heading:
	case PreparedBlockKind::CodeBlock:
		return CountTextRevealLines(
			DisplayedEditableLeaf(block),
			block.textRect,
			block.textWidth);
	case PreparedBlockKind::Rule:
		return CountGenericRevealBand(block.outer);
	case PreparedBlockKind::List:
	case PreparedBlockKind::ListItem:
	case PreparedBlockKind::Quote:
		return CountRevealLinesForBlocks(block.children, st);
	case PreparedBlockKind::DisplayMath:
		return CountGenericRevealBand(block.visibleFormulaRect);
	case PreparedBlockKind::Table:
		return CountTextRevealLines(
			DisplayedEditableLeaf(block),
			block.textRect,
			block.textWidth) + CountTableRevealLines(block);
	case PreparedBlockKind::Details:
		return CountTextRevealLines(
			DisplayedEditableLeaf(block),
			block.textRect,
			block.textWidth) + CountRevealLinesForBlocks(block.children, st);
	case PreparedBlockKind::Photo:
	case PreparedBlockKind::Video:
	case PreparedBlockKind::Audio:
	case PreparedBlockKind::Map:
	case PreparedBlockKind::Channel:
	case PreparedBlockKind::GroupedMedia:
	case PreparedBlockKind::Placeholder:
		return CountMediaRevealLines(block);
	case PreparedBlockKind::RelatedArticle:
		return CountGenericRevealBand(block.visibleMediaRect);
	case PreparedBlockKind::EmbedPost:
		return CountEmbedPostRevealLines(block, st);
	}
	Unexpected("Prepared block kind in CountRevealLinesForBlock.");
}

[[nodiscard]] int CountRevealLinesForBlocks(
		const std::vector<LaidOutBlock> &blocks,
		const style::Markdown &st) {
	auto result = 0;
	for (const auto &block : blocks) {
		result += CountRevealLinesForBlock(block, st);
	}
	return result;
}

template <typename CountCallback>
[[nodiscard]] int CachedRevealLineCount(
		const MarkdownArticlePaintContext &context,
		const void *key,
		CountCallback &&count) {
	const auto cache = context.reveal ? context.reveal->lineCounts : nullptr;
	if (!cache) {
		return count();
	}
	const auto i = cache->counts.find(key);
	if (i != end(cache->counts)) {
		return i->second;
	}
	const auto result = count();
	cache->counts.emplace(key, result);
	return result;
}

void AdvanceRevealLinesForBlock(
		const MarkdownArticlePaintContext &context,
		const LaidOutBlock &block,
		const style::Markdown &st) {
	if (context.reveal) {
		context.reveal->nextLine += CachedRevealLineCount(
			context,
			&block,
			[&] { return CountRevealLinesForBlock(block, st); });
	}
}

template <typename Callback>
void PaintRevealBand(
		Painter &p,
		const MarkdownArticlePaintContext &context,
		QRect band,
		Callback paint) {
	if (band.isEmpty()) {
		return;
	}
	const auto lineIndex = ConsumeRevealLine(context);
	const auto visible = context.clip.intersected(band);
	if (visible.isEmpty()) {
		return;
	}

	const auto paintDirect = [&] {
		const auto local = ClippedContext(
			RevealSuppressedContext(context),
			visible);
		p.save();
		p.setClipRect(visible, Qt::IntersectClip);
		paint(p, local);
		p.restore();
	};
	if (!lineIndex) {
		paintDirect();
		return;
	}

	const auto reveal = context.reveal;
	if (*lineIndex > reveal->activeLine) {
		return;
	} else if (*lineIndex < reveal->activeLine
		|| !reveal->postprocess
		|| !reveal->postprocess->method) {
		paintDirect();
		return;
	}

	auto postprocess = reveal->postprocess->method(*lineIndex, band.width());
	if (!postprocess) {
		paintDirect();
		return;
	}

	const auto ratio = std::max(style::DevicePixelRatio(), 1);
	const auto cacheWidth = band.width() * ratio;
	const auto cacheHeight = band.height() * ratio;
	auto &cache = *reveal->postprocess->cache;
	if (cache.devicePixelRatio() != ratio
		|| cache.width() < cacheWidth
		|| cache.height() < cacheHeight) {
		cache = QImage(
			QSize(
				std::max(cache.width(), cacheWidth),
				std::max(cache.height(), cacheHeight)),
			QImage::Format_ARGB32_Premultiplied);
		cache.setDevicePixelRatio(ratio);
	}
	cache.fill(Qt::transparent);

	{
		auto cachePainter = Painter(&cache);
		cachePainter.setFont(p.font());
		cachePainter.setPen(p.pen());
		cachePainter.setBrush(p.brush());
		cachePainter.setRenderHints(p.renderHints());
		cachePainter.setInactive(p.inactive());
		cachePainter.setTextPalette(p.textPalette());
		cachePainter.translate(-band.x(), -band.y());
		cachePainter.setClipRect(band);
		const auto local = ClippedContext(
			RevealSuppressedContext(context),
			band);
		paint(cachePainter, local);
	}

	postprocess(cache);

	p.save();
	p.setClipRect(visible, Qt::IntersectClip);
	p.drawImage(
		QRectF(band),
		cache,
		QRectF(0, 0, cacheWidth, cacheHeight));
	p.restore();
}

void EnsureThinkingPaintCacheImage(
		QImage *image,
		QSize logicalSize,
		int ratio) {
	if (!image || logicalSize.isEmpty()) {
		return;
	}
	ratio = std::max(ratio, 1);
	const auto neededSize = QSize(
		logicalSize.width() * ratio,
		logicalSize.height() * ratio);
	if (image->devicePixelRatio() == ratio
		&& image->format() == QImage::Format_ARGB32_Premultiplied
		&& image->width() >= neededSize.width()
		&& image->height() >= neededSize.height()) {
		return;
	}
	*image = QImage(
		QSize(
			std::max(image->width(), neededSize.width()),
			std::max(image->height(), neededSize.height())),
		QImage::Format_ARGB32_Premultiplied);
	image->setDevicePixelRatio(ratio);
}

[[nodiscard]] int ThinkingTextWidth(const LaidOutBlock &block) {
	return std::clamp(
		block.leaf.maxWidth(),
		1,
		std::max(block.textRect.width(), 1));
}

void FillThinkingGradientImage(
		QImage *image,
		QRect logicalRect,
		QRect textRect,
		int textWidth,
		QColor baseColor,
		QColor highlightColor) {
	if (!image) {
		return;
	}
	image->fill(Qt::transparent);
	if (logicalRect.isEmpty()) {
		return;
	}
	auto imagePainter = Painter(image);
	const auto localRect = QRect(QPoint(), logicalRect.size());
	const auto period = crl::now() % kThinkingGradientFullDuration;
	if (period >= kThinkingGradientSlideDuration) {
		imagePainter.fillRect(localRect, baseColor);
		return;
	}
	const auto progress = period / float64(kThinkingGradientSlideDuration);
	textWidth = std::max(textWidth, 1);
	const auto gradientWidth = std::max(textWidth, 1);
	const auto textLeft = textRect.x() - logicalRect.x();
	const auto start = anim::interpolate(
		textLeft - gradientWidth,
		textLeft + textWidth,
		progress);
	auto gradient = QLinearGradient(start, 0, start + gradientWidth, 0);
	gradient.setStops({
		{ 0., baseColor },
		{ 0.5, highlightColor },
		{ 1., baseColor },
	});
	imagePainter.fillRect(localRect, QBrush(gradient));
}

[[nodiscard]] QColor WithOpacity(style::color color, double opacity) {
	auto result = color->c;
	result.setAlphaF(result.alphaF() * std::clamp(opacity, 0., 1.));
	return result;
}

[[nodiscard]] QPen EditPlaceholderPen(
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	auto color = context.caches.supplementaryColorOverride.value_or(
		PaintStyle(context, st).supplementaryTextColor->c);
	color.setAlphaF(color.alphaF() * 0.5);
	return QPen(color);
}

[[nodiscard]] bool PaintEditPlaceholderLeaf(
		Painter &p,
		const Ui::Text::String &placeholderLeaf,
		bool empty,
		const MarkdownArticlePaintContext &context,
		QRect rect,
		int width,
		int segmentIndex,
		const style::Markdown &st,
		style::align align = style::al_left,
		std::optional<TextSelection> selection = std::nullopt) {
	if (!empty || placeholderLeaf.isEmpty() || rect.isEmpty()) {
		return false;
	}
	p.save();
	p.setPen(EditPlaceholderPen(st, context));
	PaintSelectableTextLeaf(
		p,
		placeholderLeaf,
		context,
		rect,
		width,
		segmentIndex,
		align,
		selection,
		0);
	p.restore();
	return true;
}

[[nodiscard]] QColor TableHeaderBg(const style::MarkdownTable &st) {
	return WithOpacity(st.headerBg, st.headerBgOpacity);
}

[[nodiscard]] bool UseIncomingMessageDefaultPalette(
		const MarkdownArticlePaintContext &context) {
	return !context.outbg
		&& (context.caches.st == &context.messageStyle()->richPageStyle);
}

[[nodiscard]] QColor ApplySelectedIncomingOverlay(
		QColor base,
		const MarkdownArticlePaintContext &context) {
	if (!context.selected() || context.outbg) {
		return base;
	}
	auto overlay = context.st->msgSelectOverlay()->c;
	const auto alpha = overlay.alphaF();
	overlay.setAlpha(base.alpha());
	auto result = anim::color(base, overlay, alpha);
	result.setAlpha(base.alpha());
	return result;
}

[[nodiscard]] QColor EffectiveTableHeaderBg(
		const style::Markdown &paintSt,
		const MarkdownArticlePaintContext &context) {
	return UseIncomingMessageDefaultPalette(context)
		? ApplySelectedIncomingOverlay(
			TableHeaderBg(st::defaultMarkdown.table),
			context)
		: TableHeaderBg(paintSt.table);
}

[[nodiscard]] QColor EffectiveTableBorderFg(
		const style::Markdown &paintSt,
		const MarkdownArticlePaintContext &context) {
	const auto useIncoming = UseIncomingMessageDefaultPalette(context);
	const auto &table = useIncoming
		? st::defaultMarkdown.table
		: paintSt.table;
	auto result = WithOpacity(table.borderFg, table.headerBgOpacity * 3);
	return useIncoming
		? ApplySelectedIncomingOverlay(result, context)
		: result;
}

[[nodiscard]] QColor EffectiveDividerFg(
		const style::Markdown &paintSt,
		const MarkdownArticlePaintContext &context) {
	auto result = UseIncomingMessageDefaultPalette(context)
		? ApplySelectedIncomingOverlay(
			st::defaultMarkdown.rule.fg->c,
			context)
		: paintSt.rule.fg->c;
	if (context.outbg) {
		result.setAlphaF(result.alphaF() * 0.5);
	}
	return result;
}

void PaintDetailsIcon(
		Painter &p,
		QRect rect,
		QColor color,
		bool collapsed) {
	const auto size = std::min(rect.width(), rect.height()) / 3.;
	if (size <= 0.) {
		return;
	}
	const auto center = QRectF(rect).center();
	const auto path = Ui::ToggleUpDownArrowPath(
		center.x(),
		center.y(),
		size,
		st::mainMenuToggleFourStrokes,
		collapsed ? 0. : 1.);
	auto hq = PainterHighQualityEnabler(p);
	p.fillPath(path, color);
}

void RefreshBlockThumbnail(
		const LaidOutBlock &block,
		const MarkdownArticlePaintContext &context) {
	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 = context.caches.repaint;
			const auto repaintRect = context.caches.repaintRect;
			const auto rect = block.mediaRect;
			image->subscribeToUpdates([repaint, repaintRect, rect] {
				if (repaintRect && !rect.isEmpty()) {
					repaintRect(rect);
				} else if (repaint) {
					repaint();
				}
			});
		}
	}
}

void RefreshRelatedArticleImages(
		const LaidOutBlock &block,
		const MarkdownArticlePaintContext &context) {
	if (!block.photoRuntime || block.thumbnailRect.isEmpty()) {
		return;
	}
	const auto size = block.thumbnailRect.size();
	RefreshResolvedBlockImage(
		block,
		context,
		size,
		&block.thumbnailRequestSize,
		&block.thumbnailImage,
		&block.previousThumbnailImage,
		&block.subscribedThumbnailImage,
		[&](QSize requested) {
			return block.photoRuntime->thumbnail(requested);
		});
	RefreshResolvedBlockImage(
		block,
		context,
		size,
		&block.fullRequestSize,
		&block.fullImage,
		&block.previousFullImage,
		&block.subscribedFullImage,
		[&](QSize requested) {
			return block.photoRuntime->full(requested);
		});
}

void PaintTextLeaf(
		Painter &p,
		const Ui::Text::String &leaf,
		const MarkdownArticlePaintContext &context,
		QRect rect,
		int width,
		style::align align = style::al_left,
		std::optional<TextSelection> selection = std::nullopt,
		int elisionLines = 0) {
	const auto availableWidth = std::max(width, 1);
	auto linePostprocess = std::optional<Ui::Text::LinePostprocess>();
	if (context.reveal && !elisionLines) {
		const auto lineCount = CachedRevealLineCount(
			context,
			&leaf,
			[&] {
				return int(leaf.countLinesGeometry(
					availableWidth,
					true).size());
			});
		const auto baseLine = context.reveal->nextLine;
		context.reveal->nextLine += lineCount;
		if (const auto articlePostprocess = context.reveal->postprocess) {
			const auto activeLine = context.reveal->activeLine;
			linePostprocess.emplace(Ui::Text::LinePostprocess{
				.method = [=](int lineIndex) -> Fn<void(QImage&)> {
					const auto globalLine = baseLine + lineIndex;
					if (globalLine != activeLine
						|| !articlePostprocess->method) {
						return nullptr;
					}
					return articlePostprocess->method(
						globalLine,
						availableWidth);
				},
				.cache = articlePostprocess->cache,
			});
		}
	}
	p.save();
	if (!context.clip.isNull()) {
		p.setClipRect(context.clip, Qt::IntersectClip);
	}
	leaf.draw(p, {
		.position = rect.topLeft(),
		.availableWidth = availableWidth,
		.geometry = elisionLines
			? Ui::Text::SimpleGeometry(availableWidth, elisionLines, 0, true)
			: TextGeometry(availableWidth),
		.align = align,
		.clip = context.clip,
		.palette = &p.textPalette(),
		.pre = context.caches.pre,
		.blockquote = context.caches.blockquote,
		.colors = context.caches.colors,
		.spoiler = Ui::Text::DefaultSpoilerCache(),
		.now = context.now,
		.selection = selection.value_or(TextSelection()),
		.elisionLines = elisionLines,
		.linePostprocess = linePostprocess ? &*linePostprocess : nullptr,
	});
	p.restore();
}

void PaintSelectableTextLeaf(
		Painter &p,
		const Ui::Text::String &leaf,
		const MarkdownArticlePaintContext &context,
		QRect rect,
		int width,
		int segmentIndex,
		style::align align = style::al_left,
		std::optional<TextSelection> selection = std::nullopt,
		int elisionLines = 0) {
	if ((segmentIndex >= 0)
		&& ((context.hiddenTextSegmentIndex == segmentIndex)
			|| (context.hiddenSegmentIndex == segmentIndex))) {
		if (context.reveal && !elisionLines) {
			context.reveal->nextLine += CachedRevealLineCount(
				context,
				&leaf,
				[&] { return CountTextRevealLines(leaf, rect, width); });
		}
		return;
	}
	PaintTextLeaf(
		p,
		leaf,
		context,
		rect,
		width,
		align,
		selection,
		elisionLines);
}

[[nodiscard]] QRect FlowTextViewportRect(const LaidOutBlock &block) {
	return block.scrollViewportRect.isEmpty()
		? block.outer
		: block.scrollViewportRect;
}

void SetTextLeafPen(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto &paintSt = PaintStyle(context, st);
	p.setPen(!block.supplementary
		? paintSt.textColor->c
		: context.caches.supplementaryColorOverride.value_or(
			paintSt.supplementaryTextColor->c));
}

void PaintThinkingTextLeafDirect(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto &paintSt = PaintStyle(context, st);
	p.setPen(paintSt.supplementaryTextColor->c);
	PaintSelectableTextLeaf(
		p,
		block.leaf,
		context,
		block.textRect,
		block.textWidth,
		block.segmentIndex,
		style::al_left,
		TextSelectionForSegmentIndex(
			context.selectionState,
			block.segmentIndex));
}

void PaintRelatedArticleTextLeaf(
		Painter &p,
		const Ui::Text::String &leaf,
		const MarkdownArticlePaintContext &context,
		QRect rect,
		int width,
		int elisionLines) {
	const auto textClip = context.clip.intersected(rect);
	if (textClip.isEmpty()) {
		return;
	}
	const auto clipped = ClippedContext(context, textClip);
	p.save();
	p.setClipRect(textClip, Qt::IntersectClip);
	PaintTextLeaf(
		p,
		leaf,
		clipped,
		rect,
		width,
		style::al_left,
		std::nullopt,
		elisionLines);
	p.restore();
}

void PaintTaskMarker(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context,
		int outerWidth) {
	const auto rect = block.markerRect;
	if (rect.isEmpty()) {
		return;
	}
	const auto &paintSt = PaintStyle(context, st);
	if (block.taskMarkerRippleRuntime && block.taskMarkerRippleRuntime->ripple) {
		const auto rippleTopLeft = rect.topLeft()
			+ st::defaultCheckbox.rippleAreaPosition;
		block.taskMarkerRippleRuntime->ripple->paint(
			p,
			rippleTopLeft.x(),
			rippleTopLeft.y(),
			outerWidth);
	}
	auto view = Ui::CheckView(
		paintSt.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 &st,
		const MarkdownArticlePaintContext &context) {
	const auto radius = st.list.bulletRadius;
	if (radius <= 0) {
		return;
	}
	const auto &paintSt = PaintStyle(context, st);
	auto hq = PainterHighQualityEnabler(p);
	p.setPen(Qt::NoPen);
	p.setBrush(paintSt.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 &st) {
	return block.tableBordered ? st.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.visibleTableRect.isEmpty()) {
		return path;
	}
	if (border > 0) {
		const auto half = border / 2.;
		path.addRoundedRect(
			QRectF(block.visibleTableRect).marginsRemoved({
				half,
				half,
				half,
				half,
			}),
			radius,
			radius);
	} else {
		path.addRect(block.visibleTableRect);
	}
	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.visibleTableRect.x() + border - block.horizontalScrollLeft);
	auto separatorLeft = block.visibleTableRect.x()
		+ border
		- block.horizontalScrollLeft;
	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.visibleTableRect).marginsRemoved({
		half,
		half,
		half,
		half,
	});
	AddTableHorizontalBorderSegments(
		&path,
		block,
		ownership,
		columnLefts,
		inner,
		half);
	AddTableVerticalBorderSegments(
		&path,
		block,
		ownership,
		columnLefts,
		inner,
		half);
	return path;
}

[[nodiscard]] QRect TableRowRevealBand(
		const LaidOutBlock &block,
		const style::Markdown &st,
		int rowIndex) {
	if (block.visibleTableRect.isEmpty()
		|| rowIndex < 0
		|| rowIndex >= int(block.tableRows.size())) {
		return QRect();
	}
	const auto &row = block.tableRows[rowIndex];
	if (row.outer.height() <= 0) {
		return QRect();
	}
	const auto border = std::max(TableBorder(block, st), 0);
	const auto top = (rowIndex == 0)
		? block.visibleTableRect.y()
		: row.outer.y();
	const auto tableBottom = block.visibleTableRect.y()
		+ block.visibleTableRect.height();
	const auto bottom = std::min(
		row.outer.y() + row.outer.height() + border,
		tableBottom);
	if (bottom <= top) {
		return QRect();
	}
	return QRect(
		block.visibleTableRect.x(),
		top,
		block.visibleTableRect.width(),
		bottom - top);
}

[[nodiscard]] auto TableCellsForRowBand(
		const TableOwnershipGrid &ownership,
		int rowIndex,
		QRect rowBand) -> std::vector<const LaidOutTableCell*> {
	if (rowIndex < 0 || rowIndex >= int(ownership.size())) {
		return {};
	}
	auto result = std::vector<const LaidOutTableCell*>();
	result.reserve(ownership[rowIndex].size());
	for (const auto &slot : ownership[rowIndex]) {
		const auto cell = slot.cell;
		if (!cell || !cell->outer.intersects(rowBand)) {
			continue;
		} else if (std::find(result.begin(), result.end(), cell)
			!= result.end()) {
			continue;
		}
		result.push_back(cell);
	}
	return result;
}

[[nodiscard]] int TableCellOriginRow(
		const LaidOutBlock &block,
		const LaidOutTableCell *cell) {
	for (auto rowIndex = 0;
		rowIndex != int(block.tableRows.size());
		++rowIndex) {
		for (const auto &current : block.tableRows[rowIndex].cells) {
			if (&current == cell) {
				return rowIndex;
			}
		}
	}
	return -1;
}

[[nodiscard]] bool StructuralTableRowOverlaySelected(
		const PaintSelectionState &selectionState,
		const LaidOutTableRow &row) {
	return row.editRow
		&& StructuralTableRowSelected(selectionState, *row.editRow);
}

[[nodiscard]] bool StructuralTableCellRowOverlaySelected(
		const PaintSelectionState &selectionState,
		const LaidOutTableCell &cell) {
	if (!cell.editCell) {
		return false;
	}
	auto row = PreparedEditTableRowSource();
	row.block = cell.editCell->block;
	row.tableRowIndex = cell.editCell->tableRowIndex;
	return StructuralTableRowSelected(selectionState, row);
}

void PaintStructuralTableOverlays(
		Painter &p,
		const LaidOutBlock &block,
		const QPainterPath &shapePath,
		QRect clip,
		const MarkdownArticlePaintContext &context) {
	if (!context.selectionState.hasStructuralSelection() || clip.isEmpty()) {
		return;
	}
	p.save();
	p.setClipRect(clip, Qt::IntersectClip);
	p.setClipPath(shapePath, Qt::IntersectClip);
	for (const auto &row : block.tableRows) {
		if (!row.outer.intersects(clip)
			|| !StructuralTableRowOverlaySelected(
				context.selectionState,
				row)) {
			continue;
		}
		p.fillRect(row.outer, p.textPalette().selectOverlay);
	}
	for (const auto &row : block.tableRows) {
		if (!row.outer.intersects(clip)
			|| StructuralTableRowOverlaySelected(
				context.selectionState,
				row)) {
			continue;
		}
		for (const auto &cell : row.cells) {
			if (!cell.outer.intersects(clip)
				|| !cell.editCell
				|| StructuralTableCellRowOverlaySelected(
					context.selectionState,
					cell)
				|| !StructuralTableCellSelected(
					context.selectionState,
					*cell.editCell)) {
				continue;
			}
			p.fillRect(cell.outer, p.textPalette().selectOverlay);
		}
	}
	p.restore();
}

void PaintStructuralTableRowBandOverlays(
		Painter &p,
		const LaidOutBlock &block,
		const std::vector<const LaidOutTableCell*> &cells,
		int rowIndex,
		const QPainterPath &shapePath,
		QRect clip,
		const MarkdownArticlePaintContext &context) {
	if (!context.selectionState.hasStructuralSelection()
		|| clip.isEmpty()
		|| rowIndex < 0
		|| rowIndex >= int(block.tableRows.size())) {
		return;
	}
	const auto &row = block.tableRows[rowIndex];
	p.save();
	p.setClipRect(clip, Qt::IntersectClip);
	p.setClipPath(shapePath, Qt::IntersectClip);
	if (StructuralTableRowOverlaySelected(context.selectionState, row)) {
		p.fillRect(row.outer, p.textPalette().selectOverlay);
	}
	for (const auto cell : cells) {
		if (!cell
			|| !cell->outer.intersects(clip)
			|| !cell->editCell
			|| StructuralTableCellRowOverlaySelected(
				context.selectionState,
				*cell)
			|| !StructuralTableCellSelected(
				context.selectionState,
				*cell->editCell)) {
			continue;
		}
		p.fillRect(cell->outer, p.textPalette().selectOverlay);
	}
	p.restore();
}

void PaintTableCaption(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	if (!block.textRect.isEmpty()) {
		const auto selection = TextSelectionForSegmentIndex(
			context.selectionState,
			block.secondarySegmentIndex);
		if (!PaintEditPlaceholderLeaf(
				p,
				block.placeholderLeaf,
				block.leaf.isEmpty(),
				context,
				block.textRect,
				block.textWidth,
				block.secondarySegmentIndex,
				st,
				block.flowTextAlign,
				selection)) {
			SetTextLeafPen(p, block, st, context);
			PaintSelectableTextLeaf(
				p,
				block.leaf,
				context,
				block.textRect,
				block.textWidth,
				block.secondarySegmentIndex,
				block.flowTextAlign,
				selection);
		}
	}
}

[[nodiscard]] int OverflowIndicatorWidth(
		const LaidOutBlock &block,
		const style::Markdown &st) {
	return (block.kind == PreparedBlockKind::DisplayMath)
		? st.displayMath.overflowWidth
		: st.table.overflowWidth;
}

[[nodiscard]] QColor ScrollOwnerAccentColor(
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto &paintSt = PaintStyle(context, st);
	switch (block.kind) {
	case PreparedBlockKind::Table:
		return paintSt.table.overflowFg->c;
	case PreparedBlockKind::DisplayMath:
		return paintSt.displayMath.overflowFg->c;
	case PreparedBlockKind::CodeBlock:
		return paintSt.textPalette.monoFg->c;
	case PreparedBlockKind::Quote:
		return NonPullquoteQuoteCaptionColor(context, st);
	case PreparedBlockKind::Details:
		return paintSt.supplementaryTextColor->c;
	case PreparedBlockKind::EmbedPost:
		return paintSt.embedPost.accentFg->c;
	case PreparedBlockKind::Paragraph:
	case PreparedBlockKind::Thinking:
	case PreparedBlockKind::Heading:
	case PreparedBlockKind::Rule:
	case PreparedBlockKind::List:
	case PreparedBlockKind::ListItem:
	case PreparedBlockKind::Photo:
	case PreparedBlockKind::Video:
	case PreparedBlockKind::Audio:
	case PreparedBlockKind::Map:
	case PreparedBlockKind::Channel:
	case PreparedBlockKind::GroupedMedia:
	case PreparedBlockKind::RelatedArticle:
	case PreparedBlockKind::Placeholder:
		return paintSt.table.overflowFg->c;
	}
	return paintSt.table.overflowFg->c;
}

[[nodiscard]] bool HorizontalRightEdgeHidden(const LaidOutBlock &block) {
	return block.horizontalScrollMax > 0
		&& (block.horizontalScrollLeft < block.horizontalScrollMax);
}

[[nodiscard]] QRect HorizontalScrollLogicalPaintRect(
		const LaidOutBlock &block) {
	if (block.insideHorizontalScroll) {
		return block.logicalGeometry.outer.translated(
			block.horizontalScrollAncestorShift,
			0);
	}
	return block.outer;
}

void PaintHorizontalOverflowIndicators(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context,
		QRect viewport,
		const QPainterPath *clipPath = nullptr) {
	if (block.horizontalScrollMax <= 0 || viewport.isEmpty()) {
		return;
	}
	const auto indicatorWidth = std::min(
		std::max(OverflowIndicatorWidth(block, st), 1),
		viewport.width());
	if (indicatorWidth <= 0) {
		return;
	}
	const auto left = (block.horizontalScrollLeft > 0);
	const auto right = HorizontalRightEdgeHidden(block);
	if (!left && !right) {
		return;
	}
	const auto color = ScrollOwnerAccentColor(block, st, context);
	p.save();
	p.setClipRect(viewport, Qt::IntersectClip);
	if (clipPath) {
		p.setClipPath(*clipPath, Qt::IntersectClip);
	}
	if (left) {
		p.fillRect(
			QRect(
				viewport.x(),
				viewport.y(),
				indicatorWidth,
				viewport.height()),
			color);
	}
	if (right) {
		p.fillRect(
			QRect(
				viewport.x()
					+ viewport.width()
					- indicatorWidth,
				viewport.y(),
				indicatorWidth,
				viewport.height()),
			color);
	}
	p.restore();
}

void PaintHorizontalScrollbar(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	if (block.horizontalScrollMax <= 0
		|| block.scrollScrollbarTrackRect.isEmpty()) {
		return;
	}
	const auto scrollbarRect = block.scrollScrollbarTrackRect.united(
		block.scrollScrollbarThumbRect);
	const auto scrollbarClip = context.clip.intersected(scrollbarRect);
	if (scrollbarClip.isEmpty()) {
		return;
	}
	const auto &paintSt = PaintStyle(context, st);
	auto trackBg = QColor();
	auto thumbBg = QColor();
	if (block.kind == PreparedBlockKind::Table) {
		trackBg = EffectiveTableHeaderBg(paintSt, context);
		thumbBg = EffectiveTableBorderFg(paintSt, context);
	} else {
		thumbBg = ScrollOwnerAccentColor(block, st, context);
		trackBg = thumbBg;
		trackBg.setAlphaF(trackBg.alphaF() * 0.25);
	}
	const auto radius = block.scrollScrollbarTrackRect.height() / 2.;
	p.save();
	p.setClipRect(scrollbarClip, Qt::IntersectClip);
	if (radius > 0.) {
		auto hq = PainterHighQualityEnabler(p);
		p.setPen(Qt::NoPen);
		p.setBrush(trackBg);
		p.drawRoundedRect(
			QRectF(block.scrollScrollbarTrackRect),
			radius,
			radius);
		if (!block.scrollScrollbarThumbRect.isEmpty()) {
			p.setBrush(thumbBg);
			p.drawRoundedRect(
				QRectF(block.scrollScrollbarThumbRect),
				radius,
				radius);
		}
	} else {
		p.fillRect(block.scrollScrollbarTrackRect, trackBg);
		if (!block.scrollScrollbarThumbRect.isEmpty()) {
			p.fillRect(block.scrollScrollbarThumbRect, thumbBg);
		}
	}
	p.restore();
}

void PaintWholeTable(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto tableClip = context.clip.intersected(block.visibleTableRect);
	if (tableClip.isEmpty()) {
		return;
	}
	const auto tableContext = ClippedContext(context, tableClip);

	const auto border = TableBorder(block, st);
	const auto radius = st.table.radius;
	const auto shapePath = TableShapePath(block, border, radius);
	const auto &paintSt = PaintStyle(context, st);
	const auto headerBg = EffectiveTableHeaderBg(paintSt, context);
	const auto borderFg = EffectiveTableBorderFg(paintSt, context);

	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(tableClip)) {
				continue;
			}
			if (!cell.header && !striped) {
				continue;
			}
			p.fillRect(cell.outer, headerBg);
		}
	}
	p.restore();

	if (border > 0 && !block.visibleTableRect.isEmpty()) {
		const auto path = TableBorderPath(block, border, shapePath);
		auto hq = PainterHighQualityEnabler(p);
		auto pen = QPen(borderFg, border);
		p.setPen(pen);
		p.setBrush(Qt::NoBrush);
		p.drawPath(path);
	}

	p.setPen(paintSt.textColor->c);
	for (const auto &row : block.tableRows) {
		if (!row.outer.intersects(tableClip)) {
			continue;
		}
		for (const auto &cell : row.cells) {
			if (!cell.textRect.intersects(tableClip)) {
				continue;
			}
			const auto selection = TextSelectionForSegmentIndex(
				context.selectionState,
				cell.segmentIndex);
			if (!PaintEditPlaceholderLeaf(
					p,
					cell.placeholderLeaf,
					cell.leaf.isEmpty(),
					tableContext,
					cell.textRect,
					cell.textWidth,
					cell.segmentIndex,
					st,
					cell.align,
					selection)) {
				PaintSelectableTextLeaf(
					p,
					cell.leaf,
					tableContext,
					cell.textRect,
					cell.textWidth,
					cell.segmentIndex,
					cell.align,
					selection);
			}
		}
	}

	if (block.segmentIndex >= 0
		&& WholeSegmentSelected(
			context.selectionState,
			block.segmentIndex)) {
		p.save();
		p.setClipPath(shapePath, Qt::IntersectClip);
		p.fillRect(block.visibleTableRect, p.textPalette().selectOverlay);
		p.restore();
	}
	PaintStructuralTableOverlays(
		p,
		block,
		shapePath,
		tableClip,
		context);

	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.visibleTableRect,
		&shapePath);

	p.restore();
}

void PaintTableRowBand(
		Painter &p,
		const LaidOutBlock &block,
		const TableOwnershipGrid &ownership,
		int rowIndex,
		QRect rowBand,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto rowClip = context.clip.intersected(rowBand);
	if (rowClip.isEmpty()) {
		return;
	}

	const auto border = TableBorder(block, st);
	const auto radius = st.table.radius;
	const auto shapePath = TableShapePath(block, border, radius);
	const auto &paintSt = PaintStyle(context, st);
	const auto headerBg = EffectiveTableHeaderBg(paintSt, context);
	const auto borderFg = EffectiveTableBorderFg(paintSt, context);
	const auto cells = TableCellsForRowBand(ownership, rowIndex, rowBand);
	const auto rowContext = ClippedContext(
		RevealSuppressedContext(context),
		rowClip);

	p.save();
	p.setClipRect(rowClip, Qt::IntersectClip);
	p.save();
	p.setClipPath(shapePath, Qt::IntersectClip);
	for (const auto cell : cells) {
		if (!cell || !cell->outer.intersects(rowClip)) {
			continue;
		}
		const auto originRow = TableCellOriginRow(block, cell);
		const auto striped = block.tableStriped
			&& (originRow >= 0)
			&& ((originRow % 2) == 0);
		if (!cell->header && !striped) {
			continue;
		}
		p.fillRect(cell->outer, headerBg);
	}
	p.restore();

	if (border > 0 && !block.visibleTableRect.isEmpty()) {
		const auto path = TableBorderPath(block, border, shapePath);
		auto hq = PainterHighQualityEnabler(p);
		auto pen = QPen(borderFg, border);
		p.setPen(pen);
		p.setBrush(Qt::NoBrush);
		p.drawPath(path);
	}

	p.setPen(paintSt.textColor->c);
	for (const auto cell : cells) {
		if (!cell || !cell->textRect.intersects(rowClip)) {
			continue;
		}
		const auto selection = TextSelectionForSegmentIndex(
			context.selectionState,
			cell->segmentIndex);
		if (!PaintEditPlaceholderLeaf(
				p,
				cell->placeholderLeaf,
				cell->leaf.isEmpty(),
				rowContext,
				cell->textRect,
				cell->textWidth,
				cell->segmentIndex,
				st,
				cell->align,
				selection)) {
			PaintSelectableTextLeaf(
				p,
				cell->leaf,
				rowContext,
				cell->textRect,
				cell->textWidth,
				cell->segmentIndex,
				cell->align,
				selection);
		}
	}

	if (block.segmentIndex >= 0
		&& WholeSegmentSelected(
			context.selectionState,
			block.segmentIndex)) {
		p.save();
		p.setClipPath(shapePath, Qt::IntersectClip);
		p.fillRect(block.visibleTableRect, p.textPalette().selectOverlay);
		p.restore();
	}
	PaintStructuralTableRowBandOverlays(
		p,
		block,
		cells,
		rowIndex,
		shapePath,
		rowClip,
		context);

	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.visibleTableRect,
		&shapePath);

	p.restore();
}

void PaintTableBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintTableCaption(p, block, st, context);
	if (!context.reveal) {
		PaintWholeTable(p, block, st, context);
	} else {
		const auto ownership = BuildTableOwnershipGrid(block);
		for (auto rowIndex = 0;
			rowIndex != int(block.tableRows.size());
			++rowIndex) {
			const auto rowBand = TableRowRevealBand(block, st, rowIndex);
			PaintRevealBand(
				p,
				context,
				rowBand,
				[&](Painter &p, const MarkdownArticlePaintContext &rowContext) {
					PaintTableRowBand(
						p,
						block,
						ownership,
						rowIndex,
						rowBand,
						st,
						rowContext);
				});
		}
	}
	PaintHorizontalScrollbar(p, block, st, context);
}

void PaintDisplayMathBlock(
		Painter &p,
		const LaidOutBlock &block,
		std::vector<PreparedFormulaSlot> *formulas,
		std::vector<RenderedFormula> *renderedFormulas,
		MathRenderer *renderer,
		int devicePixelRatio,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintRevealBand(
		p,
		context,
		block.visibleFormulaRect,
		[&](Painter &p, const MarkdownArticlePaintContext &formulaContext) {
			if (block.segmentIndex >= 0
				&& (formulaContext.hiddenSegmentIndex == block.segmentIndex)) {
				return;
			}
			const auto &paintSt = PaintStyle(formulaContext, st);
			const auto formula = PreparedFormulaFor(formulas, block.formulaIndex);
			p.setPen(paintSt.textColor->c);
			const auto rendered = EnsureFormulaRendered(
				formula,
				FormulaRasterSlot(renderedFormulas, block.formulaIndex),
				renderer,
				devicePixelRatio,
				st);
			if (rendered.success) {
				p.drawImage(
					block.formulaRect.topLeft(),
					ColorizedDisplayFormulaImage(
						block,
						rendered,
						p.pen().color()));
			}
			if (!rendered.success) {
				if (!PaintEditPlaceholderLeaf(
						p,
						block.placeholderLeaf,
						block.copyText.trimmed().isEmpty(),
						formulaContext,
						block.textRect,
						block.textWidth,
						block.segmentIndex,
						st,
						block.formulaAlign)) {
					p.setPen(paintSt.textColor->c);
					PaintSelectableTextLeaf(
						p,
						block.fallbackLeaf,
						formulaContext,
						block.textRect,
						block.textWidth,
						block.segmentIndex,
						block.formulaAlign);
				}
			}

			if (block.segmentIndex >= 0
				&& WholeSegmentSelected(
					formulaContext.selectionState,
					block.segmentIndex)) {
				p.fillRect(
					block.visibleFormulaRect,
					p.textPalette().selectOverlay);
			}

		});
	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.scrollViewportRect.isEmpty()
			? block.visibleFormulaRect
			: block.scrollViewportRect);
	PaintHorizontalScrollbar(p, block, st, context);
}

void PaintQuoteBlock(
		Painter &p,
		const LaidOutBlock &block,
		std::vector<PreparedFormulaSlot> *formulas,
		std::vector<RenderedFormula> *renderedFormulas,
		MathRenderer *renderer,
		int devicePixelRatio,
		int outerWidth,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto quoteClip = context.clip.intersected(block.outer);
	if (quoteClip.isEmpty()) {
		return;
	}

	if (context.caches.blockquote) {
		if (!block.pullquote) {
			const auto &quoteStyle = st.body.blockquote;
			Ui::Text::ValidateQuotePaintCache(
				*context.caches.blockquote,
				quoteStyle);

			p.save();
			p.setClipRect(quoteClip);
			Ui::Text::FillQuotePaint(
				p,
				HorizontalScrollLogicalPaintRect(block),
				*context.caches.blockquote,
				quoteStyle);
			p.restore();
		}
	}

	auto local = ClippedContext(
		context,
		context.clip.intersected(block.contentRect));
	if (!block.pullquote) {
		local.caches.supplementaryColorOverride
			= NonPullquoteQuoteCaptionColor(context, st);
	}
	PaintBlocks(
		p,
		block.children,
		formulas,
		renderedFormulas,
		renderer,
		devicePixelRatio,
		outerWidth,
		st,
		local);
	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.scrollViewportRect);
	PaintHorizontalScrollbar(p, block, st, context);
}

void PaintCodeBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto clip = context.clip.intersected(block.outer);
	if (clip.isEmpty()) {
		return;
	}

	const auto &pre = st.code.pre;
	if (context.caches.pre) {
		Ui::Text::ValidateQuotePaintCache(*context.caches.pre, pre);

		p.save();
		p.setClipRect(clip);
		Ui::Text::FillQuotePaint(
			p,
			block.outer,
			*context.caches.pre,
			pre);
		p.restore();
	}

	if (!block.headerRect.isEmpty()) {
		const auto headerText = block.codeLanguage.isEmpty()
			? Ui::Integration::Instance().phraseQuoteHeaderCopy()
			: block.codeLanguage;
		const auto font = st.code.font->monospace();
		const auto availableWidth = block.outer.width()
			- pre.headerPosition.x()
			- pre.iconPosition.x()
			- (!pre.icon.empty() ? pre.icon.width() : 0);
		if (availableWidth > 0) {
			const auto position = block.outer.topLeft()
				+ pre.headerPosition;
			p.save();
			p.setClipRect(clip);
			p.setFont(font);
			p.setPen(p.textPalette().monoFg->c);
			p.drawText(
				position + QPoint(0, font->ascent),
				font->elided(headerText, availableWidth));
			p.restore();
		}
	}

	const auto textClip = context.clip.intersected(block.contentRect);
	const auto textContext = ClippedContext(context, textClip);
	p.save();
	const auto selection = TextSelectionForSegmentIndex(
		textContext.selectionState,
		block.segmentIndex);
	if (!PaintEditPlaceholderLeaf(
			p,
			block.placeholderLeaf,
			block.codeText.text.isEmpty(),
			textContext,
			block.textRect,
			block.textWidth,
			block.segmentIndex,
			st,
			style::al_left,
			selection)) {
		p.setPen(p.textPalette().monoFg->c);
		PaintSelectableTextLeaf(
			p,
			block.leaf,
			textContext,
			block.textRect,
			block.textWidth,
			block.segmentIndex,
			style::al_left,
			selection);
	}
	p.restore();
	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.scrollViewportRect.isEmpty()
			? block.contentRect
			: block.scrollViewportRect);
	PaintHorizontalScrollbar(p, block, st, context);
}

void PaintPlaceholderBlock(
		Painter &p,
		const LaidOutBlock &block,
		int outerWidth,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintRevealBand(
		p,
		context,
		block.visibleMediaRect,
		[&](Painter &p, const MarkdownArticlePaintContext &visibleContext) {
			auto hq = PainterHighQualityEnabler(p);
			if (block.activation.kind == MediaActivationKind::Embed
				&& block.placeholderRuntime) {
				const auto border = st.placeholder.border;
				const auto radius = st.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);
				p.setPen(Qt::NoPen);
				p.setBrush(st.placeholder.bg);
				p.drawRoundedRect(block.mediaRect, radius, radius);
				if (active || pressed) {
					p.setBrush(st.placeholder.bgActive);
					p.drawRoundedRect(block.mediaRect, radius, radius);
				}
				if (const auto &ripple = block.placeholderRuntime->ripple) {
					ripple->paint(
						p,
						block.mediaRect.x(),
						block.mediaRect.y(),
						outerWidth,
						&st.placeholder.rippleBg->c);
				}
				auto pen = QPen(st.placeholder.borderFg->c);
				pen.setWidth(border);
				p.setPen(pen);
				p.setBrush(Qt::NoBrush);
				p.drawRoundedRect(borderRect, radius, radius);
				if (block.placeholderRuntime->loading) {
					const auto size = QSize(
						st.placeholder.spinnerSize,
						st.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.placeholder.spinnerFg->c),
						st.placeholder.spinnerWidth);
				} else {
					p.setPen(st.placeholder.labelFgActive->c);
					PaintTextLeaf(
						p,
						block.labelLeaf,
						visibleContext,
						block.labelRect,
						block.labelWidth,
						style::al_center);
				}
			} else {
				const auto max = block.labelLeaf.maxWidth();
				const auto radius = st.placeholder.radius;
				p.setBrush(st.placeholder.bg);
				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(st.placeholder.padding),
					radius,
					radius);
				p.setPen(st.placeholder.labelFg->c);
				PaintTextLeaf(
					p,
					block.labelLeaf,
					visibleContext,
					block.labelRect,
					block.labelWidth,
					style::al_center);
			}
			if (block.segmentIndex >= 0
				&& WholeSegmentSelected(
					visibleContext.selectionState,
					block.segmentIndex)) {
				p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
			}
		});
	if (!block.textRect.isEmpty()) {
		const auto selection = TextSelectionForSegmentIndex(
			context.selectionState,
			block.secondarySegmentIndex);
		if (!PaintEditPlaceholderLeaf(
				p,
				block.placeholderLeaf,
				block.leaf.isEmpty(),
				context,
				block.textRect,
				block.textWidth,
				block.secondarySegmentIndex,
				st,
				style::al_left,
				selection)) {
			SetTextLeafPen(p, block, st, context);
			PaintSelectableTextLeaf(
				p,
				block.leaf,
				context,
				block.textRect,
				block.textWidth,
				block.secondarySegmentIndex,
				style::al_left,
				selection);
		}
	}
}

[[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 &st,
		const MarkdownArticlePaintContext &context) {
	const auto &style = st.embedPost;
	const auto paintHeader = [&](
			Painter &p,
			const MarkdownArticlePaintContext &headerContext) {
		RefreshBlockThumbnail(block, headerContext);
		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.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);
			PaintSelectableTextLeaf(
				p,
				block.labelLeaf,
				headerContext,
				block.labelRect,
				block.labelWidth,
				block.segmentIndex,
				style::al_left,
				TextSelectionForSegmentIndex(
					headerContext.selectionState,
					block.segmentIndex));
		}
		if (!block.subtitleRect.isEmpty()) {
			p.setPen(style.dateFg->c);
			PaintSelectableTextLeaf(
				p,
				block.subtitleLeaf,
				headerContext,
				block.subtitleRect,
				block.subtitleWidth,
				block.secondarySegmentIndex,
				style::al_left,
				TextSelectionForSegmentIndex(
					headerContext.selectionState,
					block.secondarySegmentIndex));
		}
	};
	if (context.reveal) {
		if (!block.headerRect.isEmpty()) {
			const auto headerBottom = block.headerRect.y()
				+ block.headerRect.height();
			PaintRevealBand(
				p,
				context,
				QRect(
					block.mediaRect.x(),
					block.mediaRect.y(),
					block.mediaRect.width(),
					headerBottom - block.mediaRect.y()),
				paintHeader);
		} else if (block.children.empty()) {
			PaintRevealBand(p, context, block.mediaRect, paintHeader);
		}
	} else {
		const auto mediaClip = context.clip.intersected(block.mediaRect);
		if (!mediaClip.isEmpty()) {
			const auto mediaContext = ClippedContext(context, mediaClip);
			p.save();
			p.setClipRect(mediaClip);
			paintHeader(p, mediaContext);
			p.restore();
		}
	}
	if (context.reveal
		&& style.accentWidth > 0
		&& !block.children.empty()) {
		const auto top = !block.headerRect.isEmpty()
			? block.headerRect.y() + block.headerRect.height()
			: block.mediaRect.y();
		const auto bottom = block.mediaRect.y() + block.mediaRect.height();
		const auto accentRect = QRect(
			block.mediaRect.x(),
			top,
			style.accentWidth,
			std::max(bottom - top, 0));
		const auto accentClip = context.clip.intersected(accentRect);
		if (!accentClip.isEmpty()) {
			p.save();
			p.setClipRect(accentClip);
			p.fillRect(accentRect, style.accentFg->c);
			p.restore();
		}
	}
	if (!block.bodyRect.isEmpty()) {
		const auto bodyContext = ClippedContext(
			context,
			context.clip.intersected(
				block.scrollViewportRect.isEmpty()
					? block.bodyRect
					: block.scrollViewportRect));
		PaintBlocks(
			p,
			block.children,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			outerWidth,
			st,
			bodyContext);
	}
	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.scrollViewportRect);
	PaintHorizontalScrollbar(p, block, st, context);
	if (!block.textRect.isEmpty()) {
		SetTextLeafPen(p, block, st, context);
		PaintSelectableTextLeaf(
			p,
			block.leaf,
			context,
			block.textRect,
			block.textWidth,
			block.tertiarySegmentIndex,
			style::al_left,
			TextSelectionForSegmentIndex(
				context.selectionState,
				block.tertiarySegmentIndex));
	}
}

void PaintMediaCaption(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	if (block.textRect.isEmpty()) {
		return;
	}
	const auto selection = TextSelectionForSegmentIndex(
		context.selectionState,
		block.secondarySegmentIndex);
	if (!PaintEditPlaceholderLeaf(
			p,
			block.placeholderLeaf,
			block.leaf.isEmpty(),
			context,
			block.textRect,
			block.textWidth,
			block.secondarySegmentIndex,
			st,
			style::al_left,
			selection)) {
		SetTextLeafPen(p, block, st, context);
		PaintSelectableTextLeaf(
			p,
			block.leaf,
			context,
			block.textRect,
			block.textWidth,
			block.secondarySegmentIndex,
			style::al_left,
			selection);
	}
}

void PaintPersistentMediaBlock(
		Painter &p,
		const LaidOutBlock &block,
		const MarkdownArticlePaintContext &context) {
	PaintRevealBand(
		p,
		context,
		block.visibleMediaRect,
		[&](Painter &p, const MarkdownArticlePaintContext &mediaContext) {
			if (block.mediaBlock) {
				block.mediaBlock->paint(p, mediaContext);
			}
			if (block.segmentIndex >= 0
				&& WholeSegmentSelected(
					mediaContext.selectionState,
					block.segmentIndex)) {
				p.fillRect(block.mediaRect, p.textPalette().selectOverlay);
			}
		});
}

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) {
		if (radius > 0) {
			auto hq = PainterHighQualityEnabler(p);
			p.setPen(Qt::NoPen);
			p.setBrush(bg->c);
			p.drawRoundedRect(rect, radius, radius);
		} else {
			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 &st,
		const MarkdownArticlePaintContext &context) {
	PaintPersistentMediaBlock(p, block, context);
	PaintMediaCaption(p, block, st, context);
}

void PaintChannelBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintPersistentMediaBlock(p, block, context);
	PaintMediaCaption(p, block, st, context);
}

void PaintRelatedArticleBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintRevealBand(
		p,
		context,
		block.visibleMediaRect,
		[&](Painter &p, const MarkdownArticlePaintContext &visibleContext) {
			const auto &style = st.relatedArticle;
			RefreshRelatedArticleImages(block, visibleContext);
			PaintCardSurface(
				p,
				block.mediaRect,
				style.border,
				style.borderFg,
				style.bg,
				style.radius);
			if (!block.thumbnailRect.isEmpty()) {
				p.fillRect(block.thumbnailRect, style.bg->c);
				if (style.thumbnailRadius > 0) {
					auto hq = PainterHighQualityEnabler(p);
					auto path = RoundedRectPath(
						block.thumbnailRect,
						style.thumbnailRadius);
					p.save();
					p.setClipPath(path, Qt::IntersectClip);
					(void)PaintRelatedArticleImage(
						p,
						block.thumbnailRect,
						block.thumbnailImage,
						block.fullImage,
						block.previousThumbnailImage,
						block.previousFullImage);
					p.restore();
				} else {
					(void)PaintRelatedArticleImage(
						p,
						block.thumbnailRect,
						block.thumbnailImage,
						block.fullImage,
						block.previousThumbnailImage,
						block.previousFullImage);
				}
			}
			if (!block.labelRect.isEmpty()) {
				p.setPen(st.textColor->c);
				PaintRelatedArticleTextLeaf(
					p,
					block.labelLeaf,
					visibleContext,
					block.labelRect,
					block.labelWidth,
					style.titleLines);
			}
			if (!block.subtitleRect.isEmpty()) {
				p.setPen(st.textColor->c);
				PaintRelatedArticleTextLeaf(
					p,
					block.subtitleLeaf,
					visibleContext,
					block.subtitleRect,
					block.subtitleWidth,
					style.subtitleLines);
			}
			if (!block.actionRect.isEmpty()) {
				p.setPen(st.supplementaryTextColor->c);
				PaintRelatedArticleTextLeaf(
					p,
					block.actionLeaf,
					visibleContext,
					block.actionRect,
					block.actionWidth,
					style.footerLines);
			}
			if (block.segmentIndex >= 0
				&& WholeSegmentSelected(
					visibleContext.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);
			}
		});
}

void PaintPhotoBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintPersistentMediaBlock(p, block, context);
	PaintMediaCaption(p, block, st, context);
}

void PaintVideoBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintPersistentMediaBlock(p, block, context);
	PaintMediaCaption(p, block, st, context);
}

void PaintMapBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintPersistentMediaBlock(p, block, context);
	PaintMediaCaption(p, block, st, context);
}

void PaintGroupedMediaBlock(
		Painter &p,
		const LaidOutBlock &block,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	PaintRevealBand(
		p,
		context,
		block.visibleMediaRect,
		[&](Painter &p, const MarkdownArticlePaintContext &mediaContext) {
			if (block.mediaBlock) {
				block.mediaBlock->paint(p, mediaContext);
				if ((block.segmentIndex >= 0)
					&& WholeSegmentSelected(
						mediaContext.selectionState,
						block.segmentIndex)) {
					const auto &style = PaintStyle(
						mediaContext,
						st).groupedMedia;
					auto overlay = p.textPalette().selectOverlay->c;
					overlay.setAlphaF(std::clamp(
						style.overlayOpacity,
						0.,
						1.));
					p.fillPath(
						RoundedRectPath(block.mediaRect, style.radius),
						overlay);
				}
			}
		});
	PaintMediaCaption(p, block, st, context);
}

void PaintDetailsBlock(
		Painter &p,
		const LaidOutBlock &block,
		std::vector<PreparedFormulaSlot> *formulas,
		std::vector<RenderedFormula> *renderedFormulas,
		MathRenderer *renderer,
		int devicePixelRatio,
		int outerWidth,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto visible = context.clip.intersected(block.outer);
	if (visible.isEmpty()) {
		return;
	}

	const auto &details = st.details;
	const auto &paintSt = PaintStyle(context, st);
	const auto lineHeight = details.border;
	const auto lineRect = QRect(
		block.outer.x(),
		block.outer.y() + block.outer.height() - lineHeight,
		block.outer.width(),
		lineHeight);
	const auto lineFg = EffectiveDividerFg(paintSt, context);

	p.save();
	p.setClipRect(visible);
	if (lineHeight > 0 && !lineRect.isEmpty()) {
		p.fillRect(lineRect, lineFg);
	}
	if (!block.iconRect.isEmpty()) {
		const auto collapsed = block.actionRect.isEmpty()
			? block.collapsed
			: !block.detailsOpen;
		PaintDetailsIcon(
			p,
			block.iconRect,
			paintSt.supplementaryTextColor->c,
			collapsed);
	}
	const auto selection = TextSelectionForSegmentIndex(
		context.selectionState,
		block.segmentIndex);
	if (!PaintEditPlaceholderLeaf(
			p,
			block.placeholderLeaf,
			block.leaf.isEmpty(),
			context,
			block.textRect,
			block.textWidth,
			block.segmentIndex,
			st,
			style::al_left,
			selection)) {
		p.setPen(paintSt.textColor->c);
		PaintSelectableTextLeaf(
			p,
			block.leaf,
			context,
			block.textRect,
			block.textWidth,
			block.segmentIndex,
			style::al_left,
			selection);
	}
	if (!block.actionRect.isEmpty()) {
		p.setPen(paintSt.supplementaryTextColor->c);
		PaintTextLeaf(
			p,
			block.actionLeaf,
			context,
			block.actionRect,
			block.actionRect.width(),
			style::al_right);
	}
	p.restore();

	if (!block.bodyRect.isEmpty()) {
		const auto bodyContext = ClippedContext(
			context,
			context.clip.intersected(block.bodyRect));
		PaintBlocks(
			p,
			block.children,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			outerWidth,
			st,
			bodyContext);
	}
	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		block.scrollViewportRect);
	PaintHorizontalScrollbar(p, block, st, context);
}

void PaintThinkingBlock(
		Painter &p,
		const LaidOutBlock &block,
		int devicePixelRatio,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	const auto viewport = FlowTextViewportRect(block);
	const auto contentClip = context.clip.intersected(viewport);
	const auto &paintSt = PaintStyle(context, st);
	const auto baseColor = paintSt.supplementaryTextColor;
	const auto selection = TextSelectionForSegmentIndex(
		context.selectionState,
		block.segmentIndex);
	const auto logicalRect = viewport;
	const auto visible = context.clip.intersected(block.outer);
	if (visible.isEmpty()) {
		AdvanceRevealLinesForBlock(context, block, st);
		return;
	}

	const auto thinking = context.caches.thinking;
	const auto pathShiftGradient = context.caches.pathShiftGradient;
	if (!thinking || !pathShiftGradient) {
		if (!contentClip.isEmpty()) {
			PaintThinkingTextLeafDirect(
				p,
				block,
				st,
				ClippedContext(context, contentClip));
		}
		PaintHorizontalOverflowIndicators(
			p,
			block,
			st,
			context,
			viewport);
		PaintHorizontalScrollbar(p, block, st, context);
		return;
	}
	if (selection && !selection->empty()) {
		if (!contentClip.isEmpty()) {
			PaintThinkingTextLeafDirect(
				p,
				block,
				st,
				ClippedContext(context, contentClip));
		}
		PaintHorizontalOverflowIndicators(
			p,
			block,
			st,
			context,
			viewport);
		PaintHorizontalScrollbar(p, block, st, context);
		return;
	}

	const auto ratio = std::max(devicePixelRatio, 1);
	const auto cacheWidth = logicalRect.width() * ratio;
	const auto cacheHeight = logicalRect.height() * ratio;
	EnsureThinkingPaintCacheImage(
		&thinking->mask,
		logicalRect.size(),
		ratio);
	EnsureThinkingPaintCacheImage(
		&thinking->gradient,
		logicalRect.size(),
		ratio);
	thinking->mask.fill(Qt::transparent);

	{
		auto maskPainter = Painter(&thinking->mask);
		maskPainter.translate(-logicalRect.topLeft());
		maskPainter.setClipRect(logicalRect);
		maskPainter.setPen(QColor(255, 255, 255));
		PaintSelectableTextLeaf(
			maskPainter,
			block.leaf,
			ClippedContext(context, logicalRect),
			block.textRect,
			block.textWidth,
			block.segmentIndex,
			style::al_left,
			TextSelection());
	}

	const auto textWidth = ThinkingTextWidth(block);
	const auto highlightColor = anim::color(
		baseColor->c,
		::st::radialFg->c,
		0.8);
	const auto localRect = QRect(QPoint(), logicalRect.size());
	const auto painted = pathShiftGradient->paint(
		[&](const Ui::PathShiftGradient::Background &) {
			FillThinkingGradientImage(
				&thinking->gradient,
				logicalRect,
				block.textRect,
				textWidth,
				baseColor->c,
				highlightColor);
			auto gradientPainter = Painter(&thinking->gradient);
			gradientPainter.setCompositionMode(
				QPainter::CompositionMode_DestinationIn);
			gradientPainter.drawImage(
				QRectF(localRect),
				thinking->mask,
				QRectF(0, 0, cacheWidth, cacheHeight));
			gradientPainter.setCompositionMode(
				QPainter::CompositionMode_SourceOver);
			return true;
		});
	if (!painted) {
		PaintHorizontalOverflowIndicators(
			p,
			block,
			st,
			context,
			viewport);
		PaintHorizontalScrollbar(p, block, st, context);
		return;
	}

	p.save();
	p.setClipRect(contentClip, Qt::IntersectClip);
	p.drawImage(
		QRectF(logicalRect),
		thinking->gradient,
		QRectF(0, 0, cacheWidth, cacheHeight));
	p.restore();
	PaintHorizontalOverflowIndicators(
		p,
		block,
		st,
		context,
		viewport);
	PaintHorizontalScrollbar(p, block, st, context);
}

void PaintStructuralBlockOverlay(
		Painter &p,
		const LaidOutBlock &block,
		const MarkdownArticlePaintContext &context) {
	if (!context.selectionState.hasStructuralSelection()
		|| block.outer.isEmpty()) {
		return;
	}
	const auto selected = (block.editBlock
			&& StructuralBlockSelected(
				context.selectionState,
				*block.editBlock))
		|| (block.editListItem
			&& StructuralListItemSelected(
				context.selectionState,
				*block.editListItem));
	if (!selected) {
		return;
	}
	const auto visible = context.clip.intersected(block.outer);
	if (visible.isEmpty()) {
		return;
	}
	p.save();
	p.setClipRect(visible, Qt::IntersectClip);
	p.fillRect(block.outer, p.textPalette().selectOverlay);
	p.restore();
}

void PaintBlock(
		Painter &p,
		const LaidOutBlock &block,
		std::vector<PreparedFormulaSlot> *formulas,
		std::vector<RenderedFormula> *renderedFormulas,
		MathRenderer *renderer,
		int devicePixelRatio,
		int outerWidth,
		const style::Markdown &st,
		const MarkdownArticlePaintContext &context) {
	if (!block.outer.intersects(context.clip)
		&& block.kind != PreparedBlockKind::Thinking) {
		return;
	}
	if (context.debugBlockGeometry && !block.outer.isEmpty()) {
		switch (reinterpret_cast<quintptr>(&block) % 4) {
		case 0:
			p.fillRect(block.outer, QColor(128, 0, 0, 32));
			break;
		case 1:
			p.fillRect(block.outer, QColor(0, 128, 0, 32));
			break;
		case 2:
			p.fillRect(block.outer, QColor(0, 0, 128, 32));
			break;
		default:
			p.fillRect(block.outer, QColor(128, 128, 128, 32));
			break;
		}
	}
	const auto &paintSt = PaintStyle(context, st);

	switch (block.kind) {
	case PreparedBlockKind::Thinking:
		PaintThinkingBlock(p, block, devicePixelRatio, st, context);
		break;
	case PreparedBlockKind::Paragraph:
	case PreparedBlockKind::Heading:
		if (!block.headerRect.isEmpty()) {
			p.fillRect(block.headerRect, paintSt.relatedArticle.headerBg->c);
		}
		{
			const auto flowContext = ClippedContext(
				context,
				context.clip.intersected(FlowTextViewportRect(block)));
			const auto selection = TextSelectionForSegmentIndex(
				flowContext.selectionState,
				block.segmentIndex);
			if (!PaintEditPlaceholderLeaf(
					p,
					block.placeholderLeaf,
					block.leaf.isEmpty(),
					flowContext,
					block.textRect,
					block.textWidth,
					block.segmentIndex,
					st,
					block.flowTextAlign,
					selection)) {
				SetTextLeafPen(p, block, st, context);
				PaintSelectableTextLeaf(
					p,
					block.leaf,
					flowContext,
					block.textRect,
					block.textWidth,
					block.segmentIndex,
					block.flowTextAlign,
					selection);
			}
		}
		PaintHorizontalOverflowIndicators(
			p,
			block,
			st,
			context,
			FlowTextViewportRect(block));
		PaintHorizontalScrollbar(p, block, st, context);
		break;
	case PreparedBlockKind::CodeBlock:
		PaintCodeBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Rule:
		PaintRevealBand(
			p,
			context,
			block.outer,
			[&](Painter &p, const MarkdownArticlePaintContext &context) {
				p.fillRect(block.outer, EffectiveDividerFg(paintSt, context));
			});
		break;
	case PreparedBlockKind::List:
		{
			const auto childContext = block.scrollViewportRect.isEmpty()
				? context
				: ClippedContext(
					context,
					context.clip.intersected(block.scrollViewportRect));
			PaintBlocks(
				p,
				block.children,
				formulas,
				renderedFormulas,
				renderer,
				devicePixelRatio,
				outerWidth,
				st,
				childContext);
			PaintHorizontalOverflowIndicators(
				p,
				block,
				st,
				context,
				block.scrollViewportRect);
			PaintHorizontalScrollbar(p, block, st, context);
		}
		break;
	case PreparedBlockKind::ListItem:
		if (!context.reveal
			|| context.reveal->activeLine >= context.reveal->nextLine) {
			const auto markerContext = RevealSuppressedContext(context);
			if (block.taskState != TaskState::None) {
				PaintTaskMarker(p, block, st, markerContext, outerWidth);
			} else if (block.listKind == ListKind::Ordered
				&& !block.markerRect.isEmpty()) {
				p.setPen(paintSt.textColor->c);
				PaintTextLeaf(
					p,
					block.marker,
					markerContext,
					block.markerRect,
					block.markerWidth);
			} else if (block.listKind == ListKind::Bullet) {
				PaintBulletMarker(p, block, st, markerContext);
			}
		}
		{
			const auto childContext = block.scrollViewportRect.isEmpty()
				? context
				: ClippedContext(
					context,
					context.clip.intersected(block.scrollViewportRect));
			PaintBlocks(
				p,
				block.children,
				formulas,
				renderedFormulas,
				renderer,
				devicePixelRatio,
				outerWidth,
				st,
				childContext);
			PaintHorizontalOverflowIndicators(
				p,
				block,
				st,
				context,
				block.scrollViewportRect);
			PaintHorizontalScrollbar(p, block, st, context);
		}
		break;
	case PreparedBlockKind::Quote:
		PaintQuoteBlock(
			p,
			block,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			outerWidth,
			st,
			context);
		break;
	case PreparedBlockKind::DisplayMath:
		PaintDisplayMathBlock(
			p,
			block,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			st,
			context);
		break;
	case PreparedBlockKind::Table:
		PaintTableBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Photo:
		PaintPhotoBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Video:
		PaintVideoBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Audio:
		PaintAudioBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Map:
		PaintMapBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Channel:
		PaintChannelBlock(p, block, st, context);
		break;
	case PreparedBlockKind::RelatedArticle:
		PaintRelatedArticleBlock(p, block, st, context);
		break;
	case PreparedBlockKind::EmbedPost:
		PaintEmbedPostBlock(
			p,
			block,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			outerWidth,
			st,
			context);
		break;
	case PreparedBlockKind::Placeholder:
		PaintPlaceholderBlock(p, block, outerWidth, st, context);
		break;
	case PreparedBlockKind::GroupedMedia:
		PaintGroupedMediaBlock(p, block, st, context);
		break;
	case PreparedBlockKind::Details:
		PaintDetailsBlock(
			p,
			block,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			outerWidth,
			st,
			context);
		break;
	}
	PaintStructuralBlockOverlay(p, block, context);
}

} // namespace

QColor NonPullquoteQuoteCaptionColor(
		const MarkdownArticlePaintContext &context,
		const style::Markdown &st) {
	if (!context.caches.blockquote) {
		return PaintStyle(context, st).supplementaryTextColor->c;
	}
	return anim::color(
		context.caches.blockquote->bg,
		context.caches.blockquote->outlines[0],
		0.9);
}

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 &st,
		const MarkdownArticlePaintContext &context) {
	for (const auto &block : blocks) {
		if (block.outer.bottom() < context.clip.top()) {
			AdvanceRevealLinesForBlock(context, block, st);
			continue;
		} else if (block.outer.top() > context.clip.bottom()) {
			break;
		}
		PaintBlock(
			p,
			block,
			formulas,
			renderedFormulas,
			renderer,
			devicePixelRatio,
			outerWidth,
			st,
			context);
	}
}

} // namespace Iv::Markdown
