ZaStoGram_desktop/Telegram/SourceFiles/iv/markdown/iv_markdown_button_row.cpp
loop-uh 5bce8c10e5 Merge upstream dev through a1ea1ddd8c (7.2.9)
Keeps the ZaStoGram MTProto layout and its unaligned-access fixes, the
Forgejo update checker and the rich-page layout contract; takes the
upstream IV editor edge restore, horizontal overflow width, round video
seek, tlottie renderer and the lib_ui text shaper.
2026-09-23 16:21:17 +03:00

1022 lines
27 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_button_row.h"
#include "api/api_bot.h"
#include "base/weak_ptr.h"
#include "core/click_handler_types.h"
#include "iv/markdown/iv_markdown_article.h"
#include "iv/markdown/iv_markdown_article_layout_blocks.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "ui/effects/animation_value.h"
#include "ui/style/style_core_scale.h"
#include "ui/color_contrast.h"
#include "styles/style_chat.h"
#include "styles/style_iv.h"
#include "styles/style_widgets.h"
#include <algorithm>
namespace Iv::Markdown {
namespace {
using ButtonType = HistoryMessageMarkupButton::Type;
using ButtonColor = HistoryMessageMarkupButton::Color;
constexpr auto kMinPrimaryLabelContrast = 1.5;
constexpr auto kLoadingGlareOpacity = 0.2;
constexpr auto kLoadingDisabledOutlineOpacity = 0.6;
constexpr auto kLoadingHighlightOpacity = 1.;
constexpr auto kLoadingGlareTimeout = crl::time(0);
constexpr auto kLoadingGlareDuration = crl::time(1100);
constexpr auto kLoadingIdleTimeout = crl::time(500);
constexpr auto kLoadingDisabledPollInterval = crl::time(300);
[[nodiscard]] const HistoryMessageMarkupButton *LookupRuntimeButton(
const std::weak_ptr<ButtonRowRuntime> &weak,
int index) {
const auto strong = weak.lock();
return (strong && (index >= 0) && (index < int(strong->buttons.size())))
? &strong->buttons[index]
: nullptr;
}
[[nodiscard]] QString LookupRuntimeElidedLabel(
const std::weak_ptr<ButtonRowRuntime> &weak,
int index) {
const auto strong = weak.lock();
return (strong
&& (index >= 0)
&& (index < int(strong->elidedLabels.size())))
? strong->elidedLabels[index]
: QString();
}
class RichPageButtonClickHandler final : public ClickHandler {
public:
RichPageButtonClickHandler(
std::weak_ptr<ButtonRowRuntime> runtime,
int index);
void onClick(ClickContext context) const override;
QString tooltip() const override;
QString copyToClipboardText() const override;
QString copyToClipboardContextItemText() const override;
private:
[[nodiscard]] const HistoryMessageMarkupButton *lookup() const;
std::weak_ptr<ButtonRowRuntime> _runtime;
int _index = 0;
};
RichPageButtonClickHandler::RichPageButtonClickHandler(
std::weak_ptr<ButtonRowRuntime> runtime,
int index)
: _runtime(std::move(runtime))
, _index(index) {
}
void RichPageButtonClickHandler::onClick(ClickContext context) const {
if (context.button != Qt::LeftButton) {
return;
}
const auto button = lookup();
if (!button) {
return;
}
const auto my = context.other.value<ClickHandlerContext>();
Api::ActivateRichPageBotButton(my, *button);
}
QString RichPageButtonClickHandler::tooltip() const {
const auto button = lookup();
return button
? RichButtonTooltip(
button->type,
button->data,
LookupRuntimeElidedLabel(_runtime, _index))
: QString();
}
QString RichPageButtonClickHandler::copyToClipboardText() const {
const auto button = lookup();
if (!button) {
return QString();
}
switch (button->type) {
case ButtonType::Url:
case ButtonType::Auth:
case ButtonType::CopyText: return QString::fromUtf8(button->data);
}
return QString();
}
QString RichPageButtonClickHandler::copyToClipboardContextItemText() const {
const auto button = lookup();
if (!button) {
return QString();
}
switch (button->type) {
case ButtonType::Url:
case ButtonType::Auth: return tr::lng_context_copy_link(tr::now);
case ButtonType::CopyText: return tr::lng_context_copy_text(tr::now);
}
return QString();
}
const HistoryMessageMarkupButton *RichPageButtonClickHandler::lookup() const {
return LookupRuntimeButton(_runtime, _index);
}
[[nodiscard]] int NaturalButtonWidth(
const LaidOutButton &button,
const style::MarkdownButtonRow &st) {
const auto icon = RichButtonIcon(button.type);
const auto extra = icon ? st.iconExtra : 0;
return std::max(
st.height,
button.label.maxWidth() + 2 * st.padding + extra);
}
// Cell boundaries are floored cumulative prefixes computed in int64, never
// an accumulated float64. With non-negative integer weights the truncating
// division is exactly the floor, so the last boundary lands on `free` with
// no rounding slack, every cell differs from its exact share by less than
// one pixel, and the errors cancel instead of drifting along the row.
// A cell that lands under `floorWidth` is then pinned at the floor, removed
// from the pool, and the width left over is redistributed over the remaining
// weights the same way, until no new cell is pinned; that keeps both the
// exact total and the ratios between the unpinned cells, which an
// independent per-cell clamp would break. The floor is dropped entirely when
// the row is narrower than `count * floorWidth + (count - 1) * spacing`,
// where it is unsatisfiable; otherwise at least one cell always stays
// unpinned, so the pinned cells never eat more than the row.
void DistributeButtonCells(
std::vector<int> *lefts,
std::vector<int> *widths,
const std::vector<int> &weights,
int available,
int spacing,
int floorWidth) {
const auto count = int(weights.size());
const auto free = std::max(available - (count - 1) * spacing, 0);
if (int64(count) * floorWidth > free) {
floorWidth = 0;
}
auto pinned = std::vector<bool>(count, false);
auto pinnedCount = 0;
while (true) {
auto total = int64();
for (auto i = 0; i != count; ++i) {
if (!pinned[i]) {
total += weights[i];
}
}
const auto share = int64(free) - int64(pinnedCount) * floorWidth;
auto prefix = int64();
auto bound = int64();
for (auto i = 0; i != count; ++i) {
if (pinned[i]) {
(*widths)[i] = floorWidth;
} else if (total > 0) {
prefix += weights[i];
const auto next = (share * prefix) / total;
(*widths)[i] = int(next - bound);
bound = next;
} else {
(*widths)[i] = 0;
}
}
auto added = 0;
for (auto i = 0; i != count; ++i) {
if (!pinned[i] && ((*widths)[i] < floorWidth)) {
pinned[i] = true;
++added;
}
}
if (!added) {
break;
}
pinnedCount += added;
}
auto left = 0;
for (auto i = 0; i != count; ++i) {
(*lefts)[i] = left;
left += (*widths)[i] + spacing;
}
}
void ApplyButtonFallbackLadder(
LaidOutButton *button,
int left,
int width,
const style::MarkdownButtonRow &st) {
const auto natural = button->label.maxWidth();
const auto icon = RichButtonIcon(button->type);
const auto extra = icon ? st.iconExtra : 0;
const auto clearance = (width - natural) / 2;
const auto iconRoom = st.iconPosition.x() + (icon ? icon->width() : 0);
const auto withIcon = icon
&& ((width >= natural + 2 * st.padding + extra)
|| ((width >= natural + 2 * st.padding)
&& (clearance >= iconRoom)));
const auto available = std::max(width - 2 * st.labelMinPadding, 0);
const auto labelWidth = std::min(natural, available);
const auto labelHeight = st.labelStyle.font->height;
button->rect = QRect(left, 0, width, st.height);
button->elided = (labelWidth < natural);
button->labelRect = QRect(
left + (width - labelWidth) / 2,
(st.height - labelHeight) / 2,
labelWidth,
labelHeight);
button->icon = withIcon ? icon : nullptr;
button->iconRect = withIcon
? QRect(
button->rect.right() + 1 - st.iconPosition.x() - icon->width(),
button->rect.top() + st.iconPosition.y(),
icon->width(),
icon->height())
: QRect();
}
[[nodiscard]] RichButtonPillColors ResolveButtonColors(
ButtonColor color,
const style::Markdown &markdownSt) {
const auto &st = markdownSt.buttonRow;
switch (color) {
case ButtonColor::Primary:
return PrimaryPillColors(
markdownSt,
st.primaryBg->c,
st.primaryRipple->c);
case ButtonColor::Success:
return {
.bg = anim::with_alpha(st.successFg->c, st.tintBgOpacity),
.ripple = anim::with_alpha(st.successFg->c, st.tintRippleOpacity),
.fg = st.successFg->c,
};
case ButtonColor::Danger:
return {
.bg = anim::with_alpha(st.dangerFg->c, st.tintBgOpacity),
.ripple = anim::with_alpha(st.dangerFg->c, st.tintRippleOpacity),
.fg = st.dangerFg->c,
};
}
return {
.bg = anim::with_alpha(st.defaultBg->c, st.defaultBgOpacity),
.ripple = anim::with_alpha(
st.defaultRipple->c,
st.defaultRippleOpacity),
.fg = st.defaultFg->c,
};
}
void PaintButtonLabel(
QPainter &p,
const LaidOutButton &button,
QRect clip,
crl::time now,
const style::TextPalette *palette) {
if (button.labelRect.isEmpty() || button.label.isEmpty()) {
return;
}
const auto available = std::max(button.labelRect.width(), 1);
button.label.draw(p, {
.position = button.labelRect.topLeft(),
.availableWidth = available,
.geometry = Ui::Text::SimpleGeometry(available, 1, 0, false),
.clip = clip,
.palette = palette,
.now = now,
.elisionLines = 1,
});
}
void PaintButtonContent(
QPainter &p,
const LaidOutButton &button,
QColor fg,
QRect clip,
crl::time now,
int outerWidth,
const style::TextPalette *palette) {
if (button.icon) {
button.icon->paint(p, button.iconRect.topLeft(), outerWidth, fg);
}
p.setPen(fg);
PaintButtonLabel(p, button, clip, now, palette);
}
void PaintButtonPill(
QPainter &p,
const LaidOutButton &button,
const RichButtonPillColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
int outerWidth,
bool eraseRipple) {
auto hq = PainterHighQualityEnabler(p);
const auto radius = st.height / 2;
p.setPen(Qt::NoPen);
p.setBrush(colors.bg);
p.drawRoundedRect(button.rect, radius, radius);
if (ripple) {
const auto mode = p.compositionMode();
if (eraseRipple) {
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
}
ripple->paint(
p,
button.rect.x(),
button.rect.y(),
outerWidth,
&colors.ripple);
p.setCompositionMode(mode);
}
}
void PaintPlainButton(
Painter &p,
const LaidOutButton &button,
const RichButtonPillColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
QRect clip,
crl::time now,
int outerWidth,
bool disabled,
RichButtonLoadingState *loading) {
PaintButtonPill(p, button, colors, ripple, st, outerWidth, false);
if (loading) {
PaintRichButtonLoading(p, loading, colors, button.rect, st.height / 2);
}
const auto primary = (button.color == ButtonColor::Primary);
const auto was = p.opacity();
if (disabled) {
p.setOpacity(was * (primary
? st.disabledPrimaryOpacity
: st.disabledOpacity));
}
PaintButtonContent(
p,
button,
colors.fg,
clip,
now,
outerWidth,
&p.textPalette());
if (disabled) {
p.setOpacity(was);
}
}
void PaintPrimaryButton(
Painter &p,
const LaidOutButton &button,
const RichButtonPillColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
QRect clip,
crl::time now,
int outerWidth,
bool disabled,
RichButtonLoadingState *loading) {
const auto palette = &p.textPalette();
PaintPunchedOutPill(
p,
button.rect,
disabled ? st.disabledPrimaryOpacity : 1.,
[&](QPainter &q) {
PaintButtonPill(
q,
button,
colors,
ripple,
st,
outerWidth,
colors.punchOut);
if (loading) {
PaintRichButtonLoading(
q,
loading,
colors,
button.rect,
st.height / 2);
}
},
[&](QPainter &q, QColor fg) {
PaintButtonContent(
q,
button,
fg,
clip,
now,
outerWidth,
palette);
});
}
void PaintOneButton(
Painter &p,
const LaidOutButton &button,
const RichButtonPillColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
QRect clip,
crl::time now,
int outerWidth,
bool disabled,
RichButtonLoadingState *loading) {
if (colors.punchOut) {
PaintPrimaryButton(
p,
button,
colors,
ripple,
st,
clip,
now,
outerWidth,
disabled,
loading);
} else {
PaintPlainButton(
p,
button,
colors,
ripple,
st,
clip,
now,
outerWidth,
disabled,
loading);
}
}
void PaintRichButtonLoadingOutline(
QPainter &p,
QRect rect,
int radius,
const QBrush &brush) {
const auto pen = st::lineWidth;
const auto half = pen / 2.;
p.setBrush(Qt::NoBrush);
p.setPen(QPen(brush, pen));
p.drawRoundedRect(
QRectF(rect).adjusted(half, half, -half, -half),
radius - half,
radius - half);
}
[[nodiscard]] bool RichButtonLoadingIdle(
not_null<RichButtonLoadingState*> state,
crl::time now) {
return (state->lastCoveringPassAt > state->lastPaintedAt)
|| (now > state->lastPaintedAt + kLoadingIdleTimeout);
}
void RichButtonLoadingTick(not_null<RichButtonLoadingState*> state) {
if (RichButtonLoadingIdle(state, crl::now())) {
state->glare.animation.stop();
state->timer.cancel();
} else if (state->repaint) {
state->repaint();
}
}
} // namespace
ButtonRowRuntime::ButtonRowRuntime(Fn<void()> repaint)
: repaint(std::move(repaint)) {
}
const style::icon *RichButtonIcon(ButtonType type) {
using TypeIcon = HistoryMessageMarkupButton::TypeIcon;
switch (HistoryMessageMarkupButton::IconOfType(type)) {
case TypeIcon::Url: return &st::msgBotKbUrlIcon;
case TypeIcon::Payment: return &st::msgBotKbPaymentIcon;
case TypeIcon::SwitchPm: return &st::msgBotKbSwitchPmIcon;
case TypeIcon::Webview: return &st::msgBotKbWebviewIcon;
case TypeIcon::Copy: return &st::msgBotKbCopyIcon;
case TypeIcon::None: return nullptr;
}
Unexpected("TypeIcon in Iv::Markdown::RichButtonIcon.");
}
QString RichButtonTooltip(
ButtonType type,
const QByteArray &data,
const QString &elidedLabel) {
if (type == ButtonType::CopyText) {
return tr::lng_bot_copy_text_tooltip(
tr::now,
lt_text,
st::wrap_rtl(QString::fromUtf8(data)));
}
const auto url = (type == ButtonType::Url || type == ButtonType::Auth)
? QString::fromUtf8(data)
: QString();
if (url.isEmpty()) {
return elidedLabel;
} else if (elidedLabel.isEmpty()) {
return url;
}
return elidedLabel + u"\n\n"_q + url;
}
int ButtonRowMinWidth(int count, const style::MarkdownButtonRow &st) {
return (count > 0)
? (count * st.height + (count - 1) * st.spacing)
: 0;
}
int ButtonRowControlReserve() {
return st::ivEditorButtonRowControlSkip
+ st::ivEditorButtonRowControlSize;
}
QRect ButtonRowControlRect(QRect outer) {
const auto size = st::ivEditorButtonRowControlSize;
return QRect(
outer.x() + outer.width() - size,
outer.y() + (outer.height() - size) / 2,
size,
size);
}
void LayoutButtonRowButtons(
std::vector<LaidOutButton> *buttons,
TableAlignment alignment,
int width,
const style::MarkdownButtonRow &st) {
Expects(buttons != nullptr);
const auto count = int(buttons->size());
if (!count) {
return;
}
auto lefts = std::vector<int>(count, 0);
auto widths = std::vector<int>(count, 0);
if (alignment == TableAlignment::None) {
DistributeButtonCells(
&lefts,
&widths,
std::vector<int>(count, 1),
width,
st.spacing,
st.height);
} else {
auto naturals = std::vector<int>(count, 0);
auto total = 0;
for (auto i = 0; i != count; ++i) {
naturals[i] = NaturalButtonWidth((*buttons)[i], st);
total += naturals[i];
}
const auto skips = (count - 1) * st.spacing;
if (total + skips > width) {
DistributeButtonCells(
&lefts,
&widths,
naturals,
width,
st.spacing,
st.height);
} else {
const auto free = width - total - skips;
auto left = (alignment == TableAlignment::Left)
? 0
: (alignment == TableAlignment::Center)
? (free / 2)
: free;
for (auto i = 0; i != count; ++i) {
lefts[i] = left;
widths[i] = naturals[i];
left += naturals[i] + st.spacing;
}
}
}
for (auto i = 0; i != count; ++i) {
ApplyButtonFallbackLadder(
&(*buttons)[i],
lefts[i],
widths[i],
st);
}
}
int ButtonRowHitIndex(
const std::vector<LaidOutButton> &buttons,
QPoint point) {
const auto count = int(buttons.size());
for (auto i = 0; i != count; ++i) {
if (buttons[i].rect.contains(point)) {
return i;
}
}
return -1;
}
void RefreshButtonRowHandlers(
const std::shared_ptr<ButtonRowRuntime> &runtime,
const PreparedButtonRowBlockData &prepared,
const std::vector<LaidOutButton> &buttons) {
if (!runtime) {
return;
}
const auto &list = prepared.buttons;
const auto count = std::min(int(list.size()), int(buttons.size()));
runtime->buttons.clear();
runtime->buttons.reserve(count);
for (auto i = 0; i != count; ++i) {
runtime->buttons.push_back(list[i].button);
}
runtime->loadingKeys.resize(count);
runtime->elidedLabels.resize(count);
for (auto i = 0; i != count; ++i) {
runtime->loadingKeys[i] = RichButtonLoadingKey(runtime->buttons[i]);
runtime->elidedLabels[i] = buttons[i].elided
? buttons[i].fullLabel
: QString();
}
runtime->handlers.resize(count);
for (auto i = 0; i != count; ++i) {
if (buttons[i].type == ButtonType::Disabled) {
runtime->handlers[i] = nullptr;
} else if (!runtime->handlers[i]) {
runtime->handlers[i]
= std::make_shared<RichPageButtonClickHandler>(runtime, i);
}
}
if (runtime->rippleIndex >= count) {
runtime->rippleIndex = -1;
runtime->ripple = nullptr;
}
}
// A punched-out pill is composed offscreen so that its label glyphs, its
// text-colored custom emoji and any icon can be erased out of the accent
// fill with CompositionMode_DestinationOut. No code here reads the surface
// behind the pill: the holes simply expose whatever the host already
// painted, which is what lets one painter serve an incoming bubble, an
// outgoing bubble and the article surface alike. The content is then drawn
// once more on the real painter with a fully transparent color: plain
// glyphs and text-colored custom emoji contribute nothing there, while
// colorful emoji and non-text-colored custom emoji repaint themselves into
// the holes they punched and so stay colorful above the fill.
void PaintPunchedOutPill(
QPainter &p,
QRect rect,
float64 contentOpacity,
const Fn<void(QPainter&)> &paintBackground,
const Fn<void(QPainter&, QColor)> &paintContent) {
const auto ratio = style::DevicePixelRatio();
auto frame = QImage(
rect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
frame.setDevicePixelRatio(ratio);
frame.fill(Qt::transparent);
{
auto q = QPainter(&frame);
q.translate(-rect.topLeft());
paintBackground(q);
q.setCompositionMode(QPainter::CompositionMode_DestinationOut);
q.setOpacity(contentOpacity);
paintContent(q, QColor(Qt::white));
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
}
p.drawImage(rect.topLeft(), frame);
const auto was = p.opacity();
p.setOpacity(was * contentOpacity);
paintContent(p, QColor(Qt::transparent));
p.setOpacity(was);
}
// A host that knows which part of the article was on screen states it as a
// band in the article's own logical coordinates, derived from knowledge it
// holds independently of the paint clip. The pass covered everything that
// could have painted a loading pill when the clip contains that band across
// the article's full laid-out width. A host that states no band is judged by
// the whole laid-out rect instead, which is what a host that paints the
// whole article it lays out means.
bool RichButtonLoadingPassCovered(
RichButtonLoadingCoverage coverage,
QRect laidOut,
QRect clip) {
const auto covered = (coverage.bottom > coverage.top)
? laidOut.intersected(QRect(
laidOut.x(),
coverage.top,
laidOut.width(),
coverage.bottom - coverage.top))
: laidOut;
return !covered.isEmpty() && clip.contains(covered);
}
QByteArray RichButtonLoadingKey(
const HistoryMessageMarkupButton &button) {
return HistoryMessageMarkupButton::LoadsOnActivate(button.type)
? HistoryMessageMarkupButton::RichPageButtonKey(button)
: QByteArray();
}
RichButtonLoadingState *RichButtonLoadingActive(
const RichButtonLoading &loading,
const QByteArray &key) {
if (!loading.state || !loading.owner || key.isEmpty()) {
return nullptr;
}
const auto record = HistoryMessageMarkupButton::GetRichPageButton(
loading.owner,
loading.itemId,
key);
if (!record) {
return nullptr;
}
const auto type = [&]() -> std::optional<Api::BotCallbackButtonType> {
using Type = HistoryMessageMarkupButton::Type;
switch (record->type) {
case Type::Callback:
return Api::BotCallbackButtonType::Callback;
case Type::CallbackWithPassword:
return Api::BotCallbackButtonType::CallbackWithPassword;
case Type::Game:
return Api::BotCallbackButtonType::Game;
default:
return std::nullopt;
}
}();
return type && loading.owner->session().botCallbacks().buttonLoading({
.messageId = loading.itemId,
.row = -1,
.column = -1,
.type = *type,
.data = record->data,
.richPageKey = key,
}) ? loading.state : nullptr;
}
// While the glare runs, the outline's opacity is the travelling gradient's
// own horizontal profile rather than a constant, so the border is bright
// only where the band is. Everything is confined to the pill by
// construction, so no offscreen mask is needed. On a punched-out pill there
// is no resolved foreground to borrow, so the effect erases out of the
// accent fill instead, exactly as its label and its ripple already do.
void PaintRichButtonLoading(
QPainter &p,
not_null<RichButtonLoadingState*> state,
const RichButtonPillColors &colors,
QRect rect,
int radius) {
state->lastPaintedAt = crl::now();
const auto erase = colors.punchOut;
const auto color = erase ? QColor(Qt::white) : colors.fg;
auto hq = PainterHighQualityEnabler(p);
const auto mode = p.compositionMode();
const auto guard = gsl::finally([&] {
p.setCompositionMode(mode);
});
if (erase) {
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
}
if (anim::Disabled()) {
state->glare.animation.stop();
if (!state->timer.isActive()) {
const auto raw = state.get();
state->timer.setCallback([raw] { RichButtonLoadingTick(raw); });
state->timer.callEach(kLoadingDisabledPollInterval);
}
PaintRichButtonLoadingOutline(
p,
rect,
radius,
anim::with_alpha(color, kLoadingDisabledOutlineOpacity));
return;
}
state->timer.cancel();
if (!state->glare.animation.animating()) {
const auto raw = state.get();
state->glare.glare = {};
state->glare.validate(
color,
[raw] { RichButtonLoadingTick(raw); },
kLoadingGlareTimeout,
kLoadingGlareDuration);
return;
} else if (!state->glare.glare.birthTime) {
return;
}
const auto progress = std::clamp(
state->glare.progress(crl::now()),
0.,
1.);
const auto band = rect.width();
const auto left = rect.x() - band + (band * 2) * progress;
const auto stops = state->glare.computeGradient(color).stops();
const auto sweep = [&](float64 opacity) {
auto result = QLinearGradient(left, 0, left + band, 0);
auto copy = stops;
copy[1].second = anim::with_alpha(color, opacity);
result.setStops(copy);
return result;
};
p.setPen(Qt::NoPen);
p.setBrush(QBrush(sweep(kLoadingGlareOpacity)));
p.drawRoundedRect(rect, radius, radius);
PaintRichButtonLoadingOutline(
p,
rect,
radius,
QBrush(sweep(kLoadingHighlightOpacity)));
}
RichButtonPillColors BubbleGradientPillColors(
const style::Markdown &st,
float64 tintBgOpacity,
bool primary) {
const auto foreground = st.textColor->c;
const auto ripple = anim::with_alpha(
foreground,
st.buttonRow.tintRippleOpacity);
return primary
? RichButtonPillColors{
.bg = foreground,
.ripple = ripple,
.punchOut = true,
}
: RichButtonPillColors{
.bg = anim::with_alpha(foreground, tintBgOpacity),
.ripple = ripple,
.fg = foreground,
};
}
RichButtonPillColors PrimaryPillColors(
const style::Markdown &st,
QColor bg,
QColor ripple) {
const auto fg = st.buttonRow.primaryFg->c;
return (Ui::CountContrast(bg, fg) < kMinPrimaryLabelContrast)
? BubbleGradientPillColors(st, 0., true)
: RichButtonPillColors{
.bg = bg,
.ripple = ripple,
.fg = fg,
};
}
void PaintButtonRow(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &st,
const MarkdownArticlePaintContext &context,
int outerWidth) {
const auto &paintSt = context.paintMarkdownStyle(st);
const auto &style = paintSt.buttonRow;
const auto &runtime = block.buttonRowRuntime;
const auto count = int(block.buttons.size());
for (auto i = 0; i != count; ++i) {
const auto &button = block.buttons[i];
if (button.rect.isEmpty()
|| (!context.clip.isNull()
&& !button.rect.intersects(context.clip))) {
continue;
}
const auto colors = context.bubbleGradient
? BubbleGradientPillColors(
paintSt,
style.tintBgOpacity,
(button.color == ButtonColor::Primary))
: ResolveButtonColors(button.color, paintSt);
const auto disabled = (button.type == ButtonType::Disabled);
const auto ripple = (runtime
&& runtime->ripple
&& (runtime->rippleIndex == i))
? runtime->ripple.get()
: nullptr;
const auto loading = (runtime
&& (i < int(runtime->loadingKeys.size())))
? RichButtonLoadingActive(
context.buttonLoading,
runtime->loadingKeys[i])
: nullptr;
PaintOneButton(
p,
button,
colors,
ripple,
style,
context.clip,
context.now,
outerWidth,
disabled,
loading);
}
}
void PaintRichButtonPreview(
Painter &p,
const LaidOutButton &button,
const style::Markdown &st,
QRect clip,
crl::time now,
int outerWidth) {
const auto colors = ResolveButtonColors(button.color, st);
PaintOneButton(
p,
button,
colors,
nullptr,
st.buttonRow,
clip,
now,
outerWidth,
false,
nullptr);
}
void AddPillRipple(
not_null<std::unique_ptr<Ui::RippleAnimation>*> ripple,
not_null<QSize*> rippleSize,
QSize size,
QPoint point,
Fn<void()> repaint) {
if (size.isEmpty()) {
return;
} else if (!*ripple || (*rippleSize != size)) {
*ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(size, size.height() / 2),
[=] {
if (repaint) {
repaint();
}
});
*rippleSize = size;
}
point.setX(std::clamp(point.x(), 0, std::max(size.width() - 1, 0)));
point.setY(std::clamp(point.y(), 0, std::max(size.height() - 1, 0)));
(*ripple)->add(point);
if (repaint) {
repaint();
}
}
void StopPillRipple(
const std::unique_ptr<Ui::RippleAnimation> &ripple,
const Fn<void()> &repaint) {
if (!ripple) {
return;
}
ripple->lastStop();
if (repaint) {
repaint();
}
}
void AddButtonRowRipple(
const std::shared_ptr<ButtonRowRuntime> &runtime,
const std::vector<LaidOutButton> &buttons,
int index,
QPoint point) {
if (!runtime
|| (index < 0)
|| (index >= int(buttons.size()))
|| (index >= int(runtime->handlers.size()))
|| !runtime->handlers[index]) {
return;
}
const auto size = buttons[index].rect.size();
if (size.isEmpty()) {
return;
} else if (runtime->rippleIndex != index) {
runtime->ripple = nullptr;
runtime->rippleIndex = index;
}
AddPillRipple(
&runtime->ripple,
&runtime->rippleSize,
size,
point,
runtime->repaint);
}
void StopButtonRowRipple(const std::shared_ptr<ButtonRowRuntime> &runtime) {
if (!runtime) {
return;
}
StopPillRipple(runtime->ripple, runtime->repaint);
}
} // namespace Iv::Markdown