Show button rows in rich messages

Task: 2026/07/27/show-block-button-rows-in-rich-messages
This commit is contained in:
John Preston
2026-07-30 07:25:50 +04:00
parent ec688d566d
commit 45493fdfb1
35 changed files with 1704 additions and 219 deletions

View File

@@ -54,8 +54,7 @@ namespace {
void SendBotCallbackData(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column,
BotButtonLookup lookup,
std::optional<Core::CloudPasswordResult> password,
Fn<void()> done = nullptr,
Fn<void(const QString &)> handleError = nullptr) {
@@ -68,10 +67,7 @@ void SendBotCallbackData(
const auto api = &session->api();
const auto bot = item->getMessageBot();
const auto fullId = item->fullId();
const auto getButton = [=] {
return HistoryMessageMarkupButton::Get(owner, fullId, row, column);
};
const auto button = getButton();
const auto button = lookup();
if (!button || button->requestId) {
return;
}
@@ -117,7 +113,7 @@ void SendBotCallbackData(
if (!item) {
return;
}
if (const auto button = getButton()) {
if (const auto button = lookup()) {
button->requestId = 0;
owner->requestItemRepaint(item);
}
@@ -169,7 +165,7 @@ void SendBotCallbackData(
return;
}
// Show error?
if (const auto button = getButton()) {
if (const auto button = lookup()) {
button->requestId = 0;
owner->requestItemRepaint(item);
}
@@ -208,16 +204,14 @@ void HideSingleUseKeyboard(
void SendBotCallbackData(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column) {
SendBotCallbackData(controller, item, row, column, std::nullopt);
BotButtonLookup lookup) {
SendBotCallbackData(controller, item, std::move(lookup), std::nullopt);
}
void SendBotCallbackDataWithPassword(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column) {
BotButtonLookup lookup) {
if (!item->isRegular()) {
return;
}
@@ -226,21 +220,13 @@ void SendBotCallbackDataWithPassword(
const auto owner = &history->owner();
const auto api = &session->api();
const auto fullId = item->fullId();
const auto getButton = [=] {
return HistoryMessageMarkupButton::Get(
owner,
fullId,
row,
column);
};
const auto button = getButton();
if (!button || button->requestId) {
if (const auto button = lookup(); !button || button->requestId) {
return;
}
api->cloudPassword().reload();
const auto weak = base::make_weak(controller);
const auto show = controller->uiShow();
SendBotCallbackData(controller, item, row, column, {}, {}, [=](
SendBotCallbackData(controller, item, lookup, {}, {}, [=](
const QString &error) {
auto box = PrePasswordErrorBox(
error,
@@ -252,7 +238,9 @@ void SendBotCallbackDataWithPassword(
show->showBox(std::move(box), Ui::LayerOption::CloseOther);
} else {
auto lifetime = std::make_shared<rpl::lifetime>();
button->requestId = -1;
if (const auto button = lookup()) {
button->requestId = -1;
}
api->cloudPassword().state(
) | rpl::take(
1
@@ -260,7 +248,7 @@ void SendBotCallbackDataWithPassword(
if (lifetime) {
base::take(lifetime)->destroy();
}
if (const auto button = getButton()) {
if (const auto button = lookup()) {
if (button->requestId == -1) {
button->requestId = 0;
}
@@ -275,7 +263,7 @@ void SendBotCallbackDataWithPassword(
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> box) {
if (const auto button = getButton()) {
if (const auto button = lookup()) {
if (button->requestId) {
return;
}
@@ -287,15 +275,21 @@ void SendBotCallbackDataWithPassword(
if (!strongController) {
return;
}
SendBotCallbackData(strongController, item, row, column, result, [=] {
if (box) {
box->closeBox();
}
}, [=](const QString &error) {
if (box) {
box->handleCustomCheckError(error);
}
});
SendBotCallbackData(
strongController,
item,
lookup,
result,
[=] {
if (box) {
box->closeBox();
}
},
[=](const QString &error) {
if (box) {
box->handleCustomCheckError(error);
}
});
}
};
auto object = Box<PasscodeBox>(session, fields);
@@ -316,7 +310,7 @@ bool SwitchInlineBotButtonReceived(
samePeerReplyTo);
}
void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
void ActivateBotButton(ClickHandlerContext context, BotButtonLookup lookup) {
const auto strong = context.sessionWindow.get();
if (!strong) {
return;
@@ -326,11 +320,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
if (!item) {
return;
}
const auto button = HistoryMessageMarkupButton::Get(
&item->history()->owner(),
item->fullId(),
row,
column);
const auto button = lookup();
if (!button) {
return;
}
@@ -353,11 +343,11 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
case ButtonType::Callback:
case ButtonType::Game: {
SendBotCallbackData(controller, item, row, column);
SendBotCallbackData(controller, item, lookup);
} break;
case ButtonType::CallbackWithPassword: {
SendBotCallbackDataWithPassword(controller, item, row, column);
SendBotCallbackDataWithPassword(controller, item, lookup);
} break;
case ButtonType::Buy: {
@@ -514,7 +504,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
} break;
case ButtonType::Auth:
UrlAuthBox::ActivateButton(controller->uiShow(), item, row, column);
UrlAuthBox::ActivateButton(controller->uiShow(), item, lookup);
break;
case ButtonType::UserProfile: {
@@ -636,4 +626,16 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
}
}
void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
const auto strong = context.sessionWindow.get();
if (!strong) {
return;
}
const auto owner = &strong->session().data();
const auto itemId = context.itemId;
ActivateBotButton(context, [=] {
return HistoryMessageMarkupButton::Get(owner, itemId, row, column);
});
}
} // namespace Api

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
struct ClickHandlerContext;
class HistoryItem;
struct HistoryMessageMarkupButton;
namespace Window {
class SessionController;
@@ -16,17 +17,17 @@ class SessionController;
namespace Api {
using BotButtonLookup = Fn<const HistoryMessageMarkupButton*()>;
void SendBotCallbackData(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column);
BotButtonLookup lookup);
void SendBotCallbackDataWithPassword(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column);
BotButtonLookup lookup);
bool SwitchInlineBotButtonReceived(
not_null<Window::SessionController*> controller,
@@ -34,6 +35,7 @@ bool SwitchInlineBotButtonReceived(
UserData *samePeerBot = nullptr,
MsgId samePeerReplyTo = 0);
void ActivateBotButton(ClickHandlerContext context, BotButtonLookup lookup);
void ActivateBotCommand(ClickHandlerContext context, int row, int column);
} // namespace Api

View File

@@ -206,8 +206,7 @@ void RequestButton(
std::shared_ptr<Ui::Show> show,
const MTPDurlAuthResultRequest &request,
not_null<const HistoryItem*> message,
int row,
int column);
Api::BotButtonLookup lookup);
void RequestUrl(
std::shared_ptr<Ui::Show> show,
const MTPDurlAuthResultRequest &request,
@@ -218,15 +217,10 @@ void RequestUrl(
void ActivateButton(
std::shared_ptr<Ui::Show> show,
not_null<const HistoryItem*> message,
int row,
int column) {
Api::BotButtonLookup lookup) {
const auto itemId = message->fullId();
const auto button = HistoryMessageMarkupButton::Get(
&message->history()->owner(),
itemId,
row,
column);
if (button->requestId || !message->isRegular()) {
const auto button = lookup();
if (!button || button->requestId || !message->isRegular()) {
return;
}
const auto session = &message->history()->session();
@@ -243,11 +237,7 @@ void ActivateButton(
MTPstring(), // #TODO auth url
MTPstring() // in_app_origin
)).done([=](const MTPUrlAuthResult &result) {
const auto button = HistoryMessageMarkupButton::Get(
&session->data(),
itemId,
row,
column);
const auto button = lookup();
if (!button) {
return;
}
@@ -261,15 +251,11 @@ void ActivateButton(
HiddenUrlClickHandler::Open(url);
}, [&](const MTPDurlAuthResultRequest &data) {
if (const auto item = session->data().message(itemId)) {
RequestButton(show, data, item, row, column);
RequestButton(show, data, item, lookup);
}
});
}).fail([=] {
const auto button = HistoryMessageMarkupButton::Get(
&session->data(),
itemId,
row,
column);
const auto button = lookup();
if (!button) {
return;
}
@@ -320,14 +306,9 @@ void RequestButton(
std::shared_ptr<Ui::Show> show,
const MTPDurlAuthResultRequest &request,
not_null<const HistoryItem*> message,
int row,
int column) {
Api::BotButtonLookup lookup) {
const auto itemId = message->fullId();
const auto button = HistoryMessageMarkupButton::Get(
&message->history()->owner(),
itemId,
row,
column);
const auto button = lookup();
if (!button || button->requestId || !message->isRegular()) {
return;
}

View File

@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "api/api_bot.h"
class HistoryItem;
struct HistoryMessageMarkupButton;
@@ -24,8 +26,7 @@ namespace UrlAuthBox {
void ActivateButton(
std::shared_ptr<Ui::Show> show,
not_null<const HistoryItem*> message,
int row,
int column);
Api::BotButtonLookup lookup);
void ActivateUrl(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,

View File

@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class PeerData;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers

View File

@@ -40,6 +40,86 @@ namespace {
} // namespace
HistoryMessageMarkupButton::Visual ParseRichButtonVisual(
const tl::conditional<MTPRichButtonStyle> &style) {
if (!style) {
return {};
}
using Color = HistoryMessageMarkupButton::Color;
const auto &data = style->data();
return {
.color = (data.is_bg_danger()
? Color::Danger
: data.is_bg_primary()
? Color::Primary
: data.is_bg_success()
? Color::Success
: Color::Normal),
};
}
std::optional<HistoryMessageMarkupButton> ParseInlineButton(
const MTPInlineButtonType &type,
const QString &text,
HistoryMessageMarkupButton::Visual visual) {
using Type = HistoryMessageMarkupButton::Type;
auto result = std::optional<HistoryMessageMarkupButton>();
type.match([&](const MTPDinlineButtonTypeUrl &data) {
result.emplace(Type::Url, text, visual, qba(data.vurl()));
}, [&](const MTPDinlineButtonTypeUrlAuth &data) {
result.emplace(
Type::Auth,
text,
visual,
qba(data.vurl()),
qs(data.vfwd_text().value_or_empty()),
data.vbutton_id().v);
}, [&](const MTPDinputInlineButtonTypeUrlAuth &) {
LOG(("API Error: inputInlineButtonTypeUrlAuth."));
// Should not get those for the users.
}, [&](const MTPDinlineButtonTypeWebView &data) {
result.emplace(Type::WebView, text, visual, data.vurl().v);
}, [&](const MTPDinlineButtonTypeCallback &data) {
result.emplace(
(data.is_requires_password()
? Type::CallbackWithPassword
: Type::Callback),
text,
visual,
qba(data.vdata()));
}, [&](const MTPDinlineButtonTypeGame &) {
result.emplace(Type::Game, text, visual);
}, [&](const MTPDinlineButtonTypeBuy &) {
result.emplace(Type::Buy, text, visual);
}, [&](const MTPDinlineButtonTypeSwitchInline &data) {
const auto samePeer = data.is_same_peer();
result.emplace(
(samePeer ? Type::SwitchInlineSame : Type::SwitchInline),
text,
visual,
qba(data.vquery()));
if (!samePeer) {
if (const auto types = data.vpeer_types()) {
result->peerTypes = PeerTypesFromMTP(*types);
}
}
}, [&](const MTPDinlineButtonTypeUserProfile &data) {
result.emplace(
Type::UserProfile,
text,
visual,
QByteArray::number(data.vuser_id().v));
}, [&](const MTPDinputInlineButtonTypeUserProfile &) {
LOG(("API Error: inputInlineButtonTypeUserProfile."));
// Should not get those for the users.
}, [&](const MTPDinlineButtonTypeCopy &data) {
result.emplace(Type::CopyText, text, visual, data.vcopy_text().v);
}, [&](const MTPDinlineButtonTypeDisabled &) {
result.emplace(Type::Disabled, text, visual);
});
return result;
}
RequestPeerQuery RequestPeerQueryFromTL(
const MTPDbuttonTypeRequestPeer &query) {
using Type = RequestPeerQuery::Type;
@@ -116,6 +196,18 @@ HistoryMessageMarkupButton::HistoryMessageMarkupButton(
, buttonId(buttonId) {
}
bool operator==(
const HistoryMessageMarkupButton &a,
const HistoryMessageMarkupButton &b) {
return (a.type == b.type)
&& (a.visual == b.visual)
&& (a.text == b.text)
&& (a.forwardText == b.forwardText)
&& (a.data == b.data)
&& (a.buttonId == b.buttonId)
&& (a.peerTypes == b.peerTypes);
}
HistoryMessageMarkupButton *HistoryMessageMarkupButton::Get(
not_null<Data::Session*> owner,
FullMsgId itemId,
@@ -218,80 +310,19 @@ void HistoryMessageMarkupData::fillRows(
data.vurl().v);
});
}, [&](const MTPDkeyboardInlineButton &data) {
const auto text = qs(data.vtext());
const auto visual = ParseVisual(data.vstyle());
data.vtype().match([&](
const MTPDinlineButtonTypeUrl &data) {
row.emplace_back(
Type::Url,
text,
visual,
qba(data.vurl()));
}, [&](const MTPDinlineButtonTypeUrlAuth &data) {
row.emplace_back(
Type::Auth,
text,
visual,
qba(data.vurl()),
qs(data.vfwd_text().value_or_empty()),
data.vbutton_id().v);
}, [&](const MTPDinputInlineButtonTypeUrlAuth &) {
LOG(("API Error: inputInlineButtonTypeUrlAuth."));
// Should not get those for the users.
}, [&](const MTPDinlineButtonTypeWebView &data) {
row.emplace_back(
Type::WebView,
text,
visual,
data.vurl().v);
}, [&](const MTPDinlineButtonTypeCallback &data) {
row.emplace_back(
(data.is_requires_password()
? Type::CallbackWithPassword
: Type::Callback),
text,
visual,
qba(data.vdata()));
}, [&](const MTPDinlineButtonTypeGame &) {
row.emplace_back(Type::Game, text, visual);
}, [&](const MTPDinlineButtonTypeBuy &) {
row.emplace_back(Type::Buy, text, visual);
}, [&](const MTPDinlineButtonTypeSwitchInline &data) {
const auto type = data.is_same_peer()
? Type::SwitchInlineSame
: Type::SwitchInline;
row.emplace_back(
type,
text,
visual,
qba(data.vquery()));
if (type == Type::SwitchInline) {
// Optimization flag.
// Fast check on all new messages if there is a switch button to auto-click it.
flags |= ReplyMarkupFlag::HasSwitchInlineButton;
if (const auto types = data.vpeer_types()) {
row.back().peerTypes = PeerTypesFromMTP(
*types);
}
}
}, [&](const MTPDinlineButtonTypeUserProfile &data) {
row.emplace_back(
Type::UserProfile,
text,
visual,
QByteArray::number(data.vuser_id().v));
}, [&](const MTPDinputInlineButtonTypeUserProfile &) {
LOG(("API Error: inputInlineButtonTypeUserProfile."));
// Should not get those for the users.
}, [&](const MTPDinlineButtonTypeCopy &data) {
row.emplace_back(
Type::CopyText,
text,
visual,
data.vcopy_text().v);
}, [&](const MTPDinlineButtonTypeDisabled &) {
row.emplace_back(Type::Disabled, text, visual);
});
auto button = ParseInlineButton(
data.vtype(),
qs(data.vtext()),
ParseVisual(data.vstyle()));
if (!button) {
return;
}
if (button->type == Type::SwitchInline) {
// Optimization flag.
// Fast check on all new messages if there is a switch button to auto-click it.
flags |= ReplyMarkupFlag::HasSwitchInlineButton;
}
row.push_back(std::move(*button));
});
}
if (!row.empty()) {

View File

@@ -9,6 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/flags.h"
#include "data/data_chat_participant_status.h"
#include "data/data_types.h"
#include <optional>
namespace Api {
struct SendOptions;
@@ -111,6 +114,10 @@ struct HistoryMessageMarkupButton {
struct Visual {
DocumentId iconId = 0;
Color color = Color::Normal;
friend inline bool operator==(
const Visual &,
const Visual &) = default;
};
HistoryMessageMarkupButton(
@@ -137,6 +144,17 @@ struct HistoryMessageMarkupButton {
};
[[nodiscard]] bool operator==(
const HistoryMessageMarkupButton &a,
const HistoryMessageMarkupButton &b);
[[nodiscard]] std::optional<HistoryMessageMarkupButton> ParseInlineButton(
const MTPInlineButtonType &type,
const QString &text,
HistoryMessageMarkupButton::Visual visual);
[[nodiscard]] HistoryMessageMarkupButton::Visual ParseRichButtonVisual(
const tl::conditional<MTPRichButtonStyle> &style);
struct HistoryMessageMarkupData {
HistoryMessageMarkupData() = default;
explicit HistoryMessageMarkupData(const MTPReplyMarkup *data);

View File

@@ -2029,6 +2029,10 @@ void Element::validateText() {
runtime->handlerMediaActivation = {};
runtime->handlerPlaceholderId = {};
runtime->handlerPlaceholderPoint = QPoint();
runtime->handlerButtonRow = {};
runtime->handlerButtonRowHandler = nullptr;
runtime->pressedButtonRow = {};
runtime->pressedButtonRowHandler = nullptr;
invalidateTextSizeCache();
};
const auto item = data();

View File

@@ -2026,7 +2026,14 @@ QString ListWidget::tooltipText() const {
return forwarded->text.toString();
}
} else if (const auto link = ClickHandler::getActive()) {
return link->tooltip();
if (const auto text = link->tooltip(); !text.isEmpty()) {
return text;
}
}
if (const auto view = _overElement) {
auto request = StateRequest();
request.flags |= Ui::Text::StateRequest::Flag::LookupCustomTooltip;
return view->textState(_overState.point, request).customTooltipText;
}
return QString();
}
@@ -4620,7 +4627,8 @@ void ListWidget::mouseActionUpdate() {
}
if (dragState.link
|| dragState.cursor == CursorState::Date
|| dragState.cursor == CursorState::Forwarded) {
|| dragState.cursor == CursorState::Forwarded
|| dragState.customTooltip) {
Ui::Tooltip::Show(1000, this);
}

View File

@@ -3344,7 +3344,9 @@ void Message::clickHandlerPressedChanged(
} else if (const auto rich = richpage()
; rich
&& ((handler == rich->handler)
|| (handler == rich->handlerHorizontalScrollPressed))) {
|| (handler == rich->handlerHorizontalScrollPressed)
|| (handler == rich->handlerButtonRowHandler)
|| (handler == rich->pressedButtonRowHandler))) {
if (pressed) {
if ((handler == rich->handler)
&& rich->handlerHorizontalScrollHit
@@ -3369,6 +3371,21 @@ void Message::clickHandlerPressedChanged(
rich->article.stopPlaceholderRipple(rich->handlerPlaceholderId);
}
}
if (pressed) {
if ((handler == rich->handlerButtonRowHandler)
&& (rich->handlerButtonRow.index >= 0)) {
rich->pressedButtonRow = rich->handlerButtonRow;
rich->pressedButtonRowHandler = handler;
rich->article.addButtonRowRipple(
rich->pressedButtonRow.id,
rich->pressedButtonRow.index,
rich->pressedButtonRow.localPoint);
}
} else if (handler == rich->pressedButtonRowHandler) {
rich->article.stopButtonRowRipple(rich->pressedButtonRow.id);
rich->pressedButtonRow = {};
rich->pressedButtonRowHandler = nullptr;
}
} else if (_reactions) {
_reactions->clickHandlerPressedChanged(
handler,
@@ -4511,6 +4528,10 @@ bool Message::getStateText(
rich->handlerHorizontalScrollHit = std::nullopt;
rich->handlerHorizontalScrollPoint = {};
};
const auto clearButtonRowHandler = [&] {
rich->handlerButtonRow = {};
rich->handlerButtonRowHandler = nullptr;
};
const auto horizontalScrollHit = rich->article.horizontalScrollHit(local);
*outResult = TextState(item);
outResult->horizontalScroll = horizontalScrollHit.scrollable;
@@ -4523,6 +4544,7 @@ bool Message::getStateText(
rich->handlerMediaActivation = {};
rich->handlerPlaceholderId = {};
rich->handlerPlaceholderPoint = {};
clearButtonRowHandler();
if (!rich->handlerHorizontalScrollHit || !rich->handler) {
rich->handler = std::make_shared<RichPageActionClickHandler>(
[](ClickContext) {
@@ -4536,6 +4558,7 @@ bool Message::getStateText(
if (!hit.valid()) {
rich->handlerCodeHeaderSegmentIndex = -1;
clearHorizontalScrollHandler();
clearButtonRowHandler();
return horizontalScrollHit.scrollable;
}
const auto offset = rich->article.selectionOffsetFromHit(
@@ -4554,6 +4577,7 @@ bool Message::getStateText(
rich->handlerMediaActivation = {};
rich->handlerPlaceholderId = {};
rich->handlerPlaceholderPoint = {};
clearButtonRowHandler();
if (!reuse) {
const auto text = rich->article.textForContext(hit);
rich->handlerCodeHeaderSegmentIndex = hit.segmentIndex;
@@ -4573,6 +4597,7 @@ bool Message::getStateText(
rich->handlerMediaActivation = {};
rich->handlerPlaceholderId = {};
rich->handlerPlaceholderPoint = {};
clearButtonRowHandler();
outResult->link = hit.state.link;
} else if (hit.preparedLink
|| hit.mediaActivation.kind != MediaActivationKind::None) {
@@ -4588,6 +4613,7 @@ bool Message::getStateText(
clearHorizontalScrollHandler();
rich->handlerPlaceholderId = hit.mediaActivation.placeholderId;
rich->handlerPlaceholderPoint = hit.placeholderLocalPoint;
clearButtonRowHandler();
if (!reuse) {
rich->handlerPreparedLink = prepared;
rich->handlerMediaActivation = activation;
@@ -4613,6 +4639,19 @@ bool Message::getStateText(
} else {
rich->handlerCodeHeaderSegmentIndex = -1;
clearHorizontalScrollHandler();
if (hit.buttonRow.index >= 0) {
rich->handlerButtonRow = hit.buttonRow;
rich->handlerButtonRowHandler = hit.state.link;
} else {
clearButtonRowHandler();
}
if (!hit.customTooltip.isEmpty()) {
outResult->customTooltip = true;
using Flag = Ui::Text::StateRequest::Flag;
if (request.flags & Flag::LookupCustomTooltip) {
outResult->customTooltipText = hit.customTooltip;
}
}
outResult->link = hit.state.link;
}
outResult->cursor = (!outResult->link && hit.direct)

View File

@@ -112,6 +112,10 @@ struct HistoryMessageRichPage
mutable Iv::Markdown::MediaActivation handlerMediaActivation;
mutable Iv::Markdown::PreparedPlaceholderBlockId handlerPlaceholderId;
mutable QPoint handlerPlaceholderPoint;
mutable Iv::Markdown::MarkdownArticleButtonRowHit handlerButtonRow;
mutable ClickHandlerPtr handlerButtonRowHandler;
mutable Iv::Markdown::MarkdownArticleButtonRowHit pressedButtonRow;
mutable ClickHandlerPtr pressedButtonRowHandler;
};
enum class BadgeRole : uchar {

View File

@@ -1464,6 +1464,7 @@ void MergeRichTextAnchors(RichText *target, RichText source) {
case BlockKind::Unsupported:
case BlockKind::Thinking:
case BlockKind::AuthorDate:
case BlockKind::ButtonRow:
case BlockKind::Embed:
case BlockKind::EmbedPost:
case BlockKind::Channel:

View File

@@ -338,6 +338,7 @@ MarkdownBlockSkips {
relatedArticle: pixels;
embedPost: pixels;
placeholder: pixels;
buttonRow: pixels;
}
MarkdownList {
indent: pixels;
@@ -439,6 +440,28 @@ MarkdownPlaceholder {
labelFg: color;
labelFgActive: color;
}
MarkdownButtonRow {
height: pixels;
spacing: pixels;
padding: pixels;
labelMinPadding: pixels;
iconExtra: pixels;
iconPosition: point;
labelStyle: TextStyle;
defaultBg: color;
defaultBgOpacity: double;
defaultRipple: color;
defaultRippleOpacity: double;
defaultFg: color;
primaryBg: color;
primaryRipple: color;
successFg: color;
dangerFg: color;
tintBgOpacity: double;
tintRippleOpacity: double;
disabledOpacity: double;
disabledPrimaryOpacity: double;
}
MarkdownPhoto {
padding: margins;
captionSkip: pixels;
@@ -584,6 +607,7 @@ Markdown {
relatedArticle: MarkdownRelatedArticle;
groupedMedia: MarkdownGroupedMedia;
failure: MarkdownFailure;
buttonRow: MarkdownButtonRow;
}
defaultMarkdownTextPalette: TextPalette(defaultTextPalette) {
@@ -623,6 +647,7 @@ defaultMarkdownBlockSkips: MarkdownBlockSkips {
relatedArticle: 0px;
embedPost: 12px;
placeholder: 12px;
buttonRow: 12px;
}
defaultMarkdownList: MarkdownList {
indent: 28px;
@@ -885,6 +910,34 @@ defaultMarkdownFailure: MarkdownFailure {
width: 360px;
skip: 12px;
}
markdownButtonRowTintBgOpacity: 0.12;
markdownButtonRowTintRippleOpacity: 0.22;
defaultMarkdownButtonRowLabelStyle: TextStyle(defaultMarkdownBodyStyle) {
font: font(13px semibold);
lineHeight: 0px;
}
defaultMarkdownButtonRow: MarkdownButtonRow {
height: 26px;
spacing: 6px;
padding: 17px;
labelMinPadding: 4px;
iconExtra: 10px;
iconPosition: point(6px, 5px);
labelStyle: defaultMarkdownButtonRowLabelStyle;
defaultBg: windowBgOver;
defaultBgOpacity: 1.0;
defaultRipple: windowBgRipple;
defaultRippleOpacity: 1.0;
defaultFg: windowBoldFg;
primaryBg: activeButtonBg;
primaryRipple: activeButtonBgRipple;
successFg: historyPeer2NameFg;
dangerFg: historyPeer1NameFg;
tintBgOpacity: markdownButtonRowTintBgOpacity;
tintRippleOpacity: markdownButtonRowTintRippleOpacity;
disabledOpacity: 0.33;
disabledPrimaryOpacity: 0.6;
}
markdownFootnoteBox: Box(defaultBox) {
buttonPadding: margins(0px, 0px, 0px, 0px);
buttonHeight: 0px;
@@ -980,6 +1033,7 @@ defaultMarkdown: Markdown {
relatedArticle: defaultMarkdownRelatedArticle;
groupedMedia: defaultMarkdownGroupedMedia;
failure: defaultMarkdownFailure;
buttonRow: defaultMarkdownButtonRow;
}
messageMarkdownBodyStyle: TextStyle(messageTextStyle) {
font: font(13px);
@@ -1052,6 +1106,7 @@ messageMarkdownBlockSkips: MarkdownBlockSkips {
relatedArticle: 0px;
embedPost: 8px;
placeholder: 8px;
buttonRow: 8px;
}
messageMarkdownQuoteBgOpacity: 0.12;
messageMarkdownDetailsIcon: icon{{ "history_down_arrow", msgInDateFg }};
@@ -1301,6 +1356,16 @@ messageMarkdownOut: Markdown(messageMarkdown) {
failure: MarkdownFailure(messageMarkdownFailure) {
label: messageMarkdownFailureLabelOut;
}
buttonRow: MarkdownButtonRow(defaultMarkdownButtonRow) {
defaultBg: msgOutReplyBarColor;
defaultBgOpacity: markdownButtonRowTintBgOpacity;
defaultRipple: msgOutReplyBarColor;
defaultRippleOpacity: markdownButtonRowTintRippleOpacity;
defaultFg: msgOutReplyBarColor;
primaryBg: msgOutReplyBarColor;
primaryRipple: msgFileOutBg;
successFg: msgOutReplyBarColor;
}
}
messageMarkdownSelected: Markdown(messageMarkdown) {
textPalette: TextPalette(inTextPaletteSelected) {
@@ -1362,6 +1427,11 @@ messageMarkdownSelected: Markdown(messageMarkdown) {
failure: MarkdownFailure(messageMarkdownFailure) {
label: messageMarkdownFailureLabelSelected;
}
buttonRow: MarkdownButtonRow(defaultMarkdownButtonRow) {
defaultFg: historyTextInFgSelected;
successFg: historyPeer2NameFgSelected;
dangerFg: historyPeer1NameFgSelected;
}
}
messageMarkdownOutSelected: Markdown(messageMarkdown) {
textPalette: TextPalette(outTextPaletteSelected) {
@@ -1423,6 +1493,17 @@ messageMarkdownOutSelected: Markdown(messageMarkdown) {
failure: MarkdownFailure(messageMarkdownFailure) {
label: messageMarkdownFailureLabelOutSelected;
}
buttonRow: MarkdownButtonRow(defaultMarkdownButtonRow) {
defaultBg: msgOutReplyBarSelColor;
defaultBgOpacity: markdownButtonRowTintBgOpacity;
defaultRipple: msgOutReplyBarSelColor;
defaultRippleOpacity: markdownButtonRowTintRippleOpacity;
defaultFg: msgOutReplyBarSelColor;
primaryBg: msgOutReplyBarSelColor;
primaryRipple: msgFileOutBgSelected;
successFg: msgOutReplyBarSelColor;
dangerFg: historyPeer1NameFgSelected;
}
}
aiComposeCardMarkdown: Markdown(messageMarkdown) {
textPadding: margins(12px, 8px, 12px, 8px);

View File

@@ -926,6 +926,7 @@ void TrimEmptyParagraphEdges(std::vector<Block> *blocks) {
return RichTextHasVisibleText(block.text) || block.date != 0;
case BlockKind::Divider:
case BlockKind::Anchor:
case BlockKind::ButtonRow:
case BlockKind::Unsupported:
case BlockKind::List:
case BlockKind::Embed:
@@ -1648,6 +1649,7 @@ void TrimEmptyParagraphEdges(std::vector<Block> *blocks) {
}
case BlockKind::Unsupported:
case BlockKind::AuthorDate:
case BlockKind::ButtonRow:
case BlockKind::Embed:
case BlockKind::EmbedPost:
case BlockKind::Channel:

View File

@@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/flat_map.h"
#include "base/qthelp_url.h"
#include "base/unixtime.h"
#include "base/variant.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
@@ -39,6 +38,7 @@ namespace {
using Block = RichPage::Block;
using BlockKind = RichPage::BlockKind;
using ButtonAlignment = RichPage::ButtonAlignment;
using GroupedMediaIntent = RichPage::GroupedMediaIntent;
using ListItem = RichPage::ListItem;
using ListKind = RichPage::ListKind;
@@ -189,6 +189,7 @@ enum class OrderedMarkerType {
const auto PhotoLargeLevels = u"ydxcwmbsa"_q;
constexpr auto kDefaultMapWidth = 400;
constexpr auto kDefaultMapHeight = 200;
constexpr auto kMaxButtonRowButtons = 8;
[[nodiscard]] int NonZeroMapWidth(int width) {
return (width > 0) ? width : kDefaultMapWidth;
@@ -240,6 +241,7 @@ struct ParseContext {
};
base::flat_map<uint64, DocumentInfo> documentInfos;
bool dropRichTextClickHandlers = false;
bool keepRichTextFormattedDates = false;
bool displayTextDiff = false;
};
@@ -258,6 +260,7 @@ using TableOccupancyGrid = std::vector<TableOccupancyRow>;
enum class RichTextParseMode {
Normal,
DropClickHandlers,
DropClickHandlersKeepDates,
};
void AccumulateTextLength(
@@ -474,6 +477,9 @@ void AccumulateBlockMetrics(
AccumulateTextLength(metrics, block.text);
AccumulateTextLength(metrics, block.caption);
AccumulateTextLength(metrics, block.formula);
for (const auto &button : block.buttons) {
AccumulateTextLength(metrics, button.text);
}
if (IsMediaKind(block.kind)) {
++metrics->mediaCount;
}
@@ -1010,7 +1016,8 @@ void RememberWebPageMedia(
if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) {
return false;
}
if (context->dropRichTextClickHandlers) {
if (context->dropRichTextClickHandlers
&& !context->keepRichTextFormattedDates) {
return true;
}
auto flags = FormattedDateFlags();
@@ -1118,8 +1125,11 @@ void RememberWebPageMedia(
auto anchorId = QString();
auto anchorIds = std::vector<QString>();
const auto wasDropClickHandlers = context->dropRichTextClickHandlers;
const auto wasKeepFormattedDates = context->keepRichTextFormattedDates;
context->dropRichTextClickHandlers
= (mode == RichTextParseMode::DropClickHandlers);
= (mode != RichTextParseMode::Normal);
context->keepRichTextFormattedDates
= (mode == RichTextParseMode::DropClickHandlersKeepDates);
const auto parsed = AppendRichText(
text,
&result,
@@ -1127,6 +1137,7 @@ void RememberWebPageMedia(
&anchorId,
&anchorIds);
context->dropRichTextClickHandlers = wasDropClickHandlers;
context->keepRichTextFormattedDates = wasKeepFormattedDates;
(void)parsed;
result.anchorId = std::move(anchorId);
result.anchorIds = std::move(anchorIds);
@@ -1621,9 +1632,40 @@ void AppendBlock(
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockButtonRow &) {
AssertIsDebug();
result->push_back(MakeBlock(BlockKind::Unsupported));
}, [&](const MTPDpageBlockButtonRow &data) {
auto parsed = MakeBlock(BlockKind::ButtonRow);
parsed.buttonAlignment = data.is_align_left()
? ButtonAlignment::Left
: data.is_align_center()
? ButtonAlignment::Center
: data.is_align_right()
? ButtonAlignment::Right
: ButtonAlignment::Stretch;
const auto &list = data.vbuttons().v;
const auto count = std::min(int(list.size()), kMaxButtonRowButtons);
parsed.buttons.reserve(count);
for (auto i = 0; i != count; ++i) {
const auto &fields = list[i].data();
auto button = ParseInlineButton(
fields.vtype(),
QString(),
ParseRichButtonVisual(fields.vstyle()));
if (!button) {
continue;
}
auto text = ParseRichText(
fields.vtext(),
context,
RichTextParseMode::DropClickHandlersKeepDates);
button->text = text.text.text;
parsed.buttons.push_back({
.text = std::move(text),
.button = std::move(*button),
});
}
if (!parsed.buttons.empty()) {
result->push_back(std::move(parsed));
}
}, [&](const MTPDpageBlockDocument &data) {
result->push_back(MakeDocumentBlock(
BlockKind::File,
@@ -1645,61 +1687,11 @@ void AppendBlocks(
}
}
void ExpandInlineTextObjects(TextWithEntities *text, bool withIcons) {
auto &entities = text->entities;
for (auto i = entities.begin(); i != entities.end();) {
if (i->type() != EntityType::CustomEmoji) {
++i;
continue;
}
const auto object = Markdown::ParseInlineTextObjectEntity(
i->data());
if (!object) {
++i;
continue;
}
const auto replacement = v::match(object->data, [](
const Markdown::InlineTextObjectFormulaData &data) {
return data.trimmedTex;
}, [](const Markdown::InlineTextObjectIvImageData &data) {
return data.replacementText;
});
const auto offset = i->offset();
const auto length = i->length();
const auto delta = int(replacement.size()) - length;
text->text.replace(offset, length, replacement);
for (auto &entity : entities) {
if (&entity == &*i) {
continue;
} else if (entity.offset() > offset) {
entity.shiftRight(delta);
} else if (entity.offset() + entity.length() > offset) {
entity.shrinkFromRight(-delta);
}
}
const auto formula = (object->kind
== Markdown::InlineTextObjectKind::Formula);
if (withIcons && formula && !replacement.isEmpty()) {
const auto icon = Ui::Text::IconEmoji(
&st::ivSummaryMathIcon,
replacement);
*i = EntityInText(
EntityType::CustomEmoji,
offset,
int(replacement.size()),
icon.entities.front().data());
++i;
} else {
i = entities.erase(i);
}
}
}
void AppendSummaryLine(
TextWithEntities *result,
TextWithEntities &&line,
bool withIcons) {
ExpandInlineTextObjects(&line, withIcons);
Markdown::ExpandInlineTextObjects(&line, withIcons);
TextUtilities::Trim(line);
if (line.empty()) {
return;
@@ -2040,6 +2032,11 @@ void AppendSummaryBlock(
AppendSummaryLine(result, std::move(line), withIcons);
return;
}
case BlockKind::ButtonRow:
for (const auto &button : block.buttons) {
AppendSummaryLine(result, button.text, withIcons);
}
return;
case BlockKind::List: {
auto ordered = OrderedListSequenceStart(block);
const auto step = block.orderedList.reversed ? -1 : 1;
@@ -2274,6 +2271,7 @@ std::shared_ptr<const RichPage> ParsePage(
case BlockKind::File:
case BlockKind::GroupedMedia:
case BlockKind::Map:
case BlockKind::ButtonRow:
return false;
default:
break;
@@ -2542,7 +2540,7 @@ TextWithEntities FlattenRichPageToSimpleText(const RichPage &page) {
// Code blocks are allowed at the top level as a Pre entity, but
// their content is sent as plain text without any inline entities.
auto inner = block.text.text;
ExpandInlineTextObjects(&inner, false);
Markdown::ExpandInlineTextObjects(&inner, false);
inner.entities.clear();
AppendSimpleBlock(
&result,

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/basic_types.h"
#include "history/history_item_reply_markup.h"
#include "ui/text/text_entity.h"
#include <QtCore/QByteArray>
@@ -48,6 +49,7 @@ struct RichPage {
Code,
Divider,
Anchor,
ButtonRow,
List,
Quote,
Photo,
@@ -180,6 +182,23 @@ struct RichPage {
const RelatedArticle &,
const RelatedArticle &) = default;
};
enum class ButtonAlignment : uchar {
Stretch,
Left,
Center,
Right,
};
struct Button {
RichText text;
HistoryMessageMarkupButton button = HistoryMessageMarkupButton(
HistoryMessageMarkupButton::Type::Disabled,
QString(),
{});
friend inline bool operator==(
const Button &,
const Button &) = default;
};
struct Block {
BlockKind kind = BlockKind::Unsupported;
QString anchorId;
@@ -217,6 +236,7 @@ struct RichPage {
ListKind listKind = ListKind::Bullet;
OrderedListData orderedList;
GroupedMediaIntent mediaIntent = GroupedMediaIntent::Collage;
ButtonAlignment buttonAlignment = ButtonAlignment::Stretch;
PhotoData *photo = nullptr;
DocumentData *document = nullptr;
PeerData *peer = nullptr;
@@ -228,6 +248,7 @@ struct RichPage {
std::vector<GroupedMediaItem> mediaItems;
std::vector<TableRow> tableRows;
std::vector<RelatedArticle> relatedArticles;
std::vector<Button> buttons;
friend inline bool operator==(
const Block &,

View File

@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/markdown/iv_markdown_article_paint.h"
#include "iv/markdown/iv_markdown_article_selection.h"
#include "iv/markdown/iv_markdown_article_text.h"
#include "iv/markdown/iv_markdown_button_row.h"
#include "iv/markdown/iv_markdown_media_reuse.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "iv/markdown/iv_markdown_prepare_serialize.h"
@@ -606,6 +607,7 @@ void HarvestCachedTextLeafs(
&block->placeholderLeaf);
break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::Quote:
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
@@ -813,6 +815,54 @@ void CollectPlaceholderIds(
return nullptr;
}
void CollectButtonRowIds(
const std::vector<LaidOutBlock> &blocks,
std::unordered_set<uint64> *result) {
if (!result) {
return;
}
for (const auto &block : blocks) {
if (block.buttonRowId) {
result->emplace(block.buttonRowId.value);
}
CollectButtonRowIds(block.children, result);
}
}
[[nodiscard]] LaidOutBlock *FindButtonRowBlock(
std::vector<LaidOutBlock> *blocks,
PreparedMediaBlockId id) {
if (!blocks || !id) {
return nullptr;
}
for (auto &block : *blocks) {
if (block.buttonRowId.value == id.value) {
return &block;
}
if (const auto child = FindButtonRowBlock(&block.children, id)) {
return child;
}
}
return nullptr;
}
[[nodiscard]] const LaidOutBlock *FindButtonRowBlock(
const std::vector<LaidOutBlock> &blocks,
PreparedMediaBlockId id) {
if (!id) {
return nullptr;
}
for (const auto &block : blocks) {
if (block.buttonRowId.value == id.value) {
return &block;
}
if (const auto child = FindButtonRowBlock(block.children, id)) {
return child;
}
}
return nullptr;
}
void CollectTaskMarkerSources(
const std::vector<LaidOutBlock> &blocks,
TaskMarkerSourceSet *result) {
@@ -1086,6 +1136,7 @@ void AppendBlockRevealLines(
block.textWidth);
break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
AppendGenericRevealBand(lines, block.outer);
break;
case PreparedBlockKind::List:
@@ -1397,7 +1448,26 @@ void RebuildVisibleSegmentLookup(
}
};
if (segment.block) {
if (segment.block->kind == PreparedBlockKind::RelatedArticle
if (segment.block->kind == PreparedBlockKind::ButtonRow) {
const auto index = ButtonRowHitIndex(
segment.block->buttons,
point);
if (index >= 0) {
const auto &button = segment.block->buttons[index];
const auto &runtime = segment.block->buttonRowRuntime;
if (runtime && (index < int(runtime->handlers.size()))) {
result.state.link = runtime->handlers[index];
}
result.buttonRow = {
.id = segment.block->buttonRowId,
.localPoint = point - button.rect.topLeft(),
.index = index,
};
if (button.elided) {
result.customTooltip = button.fullLabel;
}
}
} else if (segment.block->kind == PreparedBlockKind::RelatedArticle
&& segment.block->preparedLink) {
result.preparedLink = segment.block->preparedLink;
result.state.link = segment.block->preparedLinkHandler;
@@ -1420,7 +1490,8 @@ void RebuildVisibleSegmentLookup(
}
}
}
result.direct = true;
result.direct = !segment.block
|| (segment.block->kind != PreparedBlockKind::ButtonRow);
return result;
}
@@ -1540,6 +1611,11 @@ void RestoreLogicalBlockGeometry(LaidOutBlock *block) {
cell.textRect = cell.logicalTextRect;
}
}
for (auto &button : block->buttons) {
button.rect = button.logicalRect;
button.labelRect = button.logicalLabelRect;
button.iconRect = button.logicalIconRect;
}
}
[[nodiscard]] bool ScrollOwnerMovesOwnContent(PreparedBlockKind kind) {
@@ -1552,6 +1628,7 @@ void RestoreLogicalBlockGeometry(LaidOutBlock *block) {
case PreparedBlockKind::Table:
return true;
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Quote:
@@ -1583,6 +1660,7 @@ void RestoreLogicalBlockGeometry(LaidOutBlock *block) {
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Photo:
@@ -1652,6 +1730,13 @@ void ApplyTranslatedDescendantGeometry(
cell.textRect = TranslateRect(cell.logicalTextRect, state.shift);
}
}
for (auto &button : block->buttons) {
button.rect = TranslateRect(button.logicalRect, state.shift);
button.labelRect = TranslateRect(
button.logicalLabelRect,
state.shift);
button.iconRect = TranslateRect(button.logicalIconRect, state.shift);
}
}
void ApplyOwnerContentGeometry(
@@ -1691,6 +1776,7 @@ void ApplyOwnerContentGeometry(
}
break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Quote:
@@ -1939,6 +2025,7 @@ void CollectMediaBlockGeometries(
case PreparedBlockKind::Thinking:
case PreparedBlockKind::Heading:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Photo:
@@ -2168,6 +2255,7 @@ void CollectMediaBlockGeometries(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Document:
@@ -2738,6 +2826,7 @@ void ConsiderStructuralBlockDropTargets(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
@@ -2801,6 +2890,7 @@ void ConsiderStructuralListItemDropTargets(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
@@ -2878,6 +2968,7 @@ void ConsiderStructuralListItemDropTargets(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
@@ -3555,6 +3646,11 @@ public:
QPoint point);
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
void addButtonRowRipple(
PreparedMediaBlockId id,
int index,
QPoint point);
void stopButtonRowRipple(PreparedMediaBlockId id);
void invalidateLayout();
@@ -3587,6 +3683,15 @@ private:
void requestPlaceholderRepaint(PreparedPlaceholderBlockId id);
void clearButtonRowRuntimes();
[[nodiscard]] auto getOrCreateButtonRowRuntime(PreparedMediaBlockId id)
-> std::shared_ptr<ButtonRowRuntime>;
void pruneButtonRowRuntimes();
void requestButtonRowRepaint(PreparedMediaBlockId id);
[[nodiscard]] auto getOrCreateTaskMarkerRippleRuntime(
const PreparedEditListItemSource &source)
-> std::shared_ptr<TaskMarkerRippleRuntime>;
@@ -3714,6 +3819,8 @@ private:
int _missingMediaBlocks = 0;
std::unordered_map<uint64, std::shared_ptr<PlaceholderBlockRuntime>>
_placeholderRuntimes;
std::unordered_map<uint64, std::shared_ptr<ButtonRowRuntime>>
_buttonRowRuntimes;
TaskMarkerRippleRuntimeMap _taskMarkerRippleRuntimes;
std::unordered_map<
uint64,
@@ -3820,6 +3927,7 @@ void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) {
clearMediaBlocks();
}
clearPlaceholderRuntimes();
clearButtonRowRuntimes();
_relatedArticleImages.clear();
_content = std::move(content);
if (reuseMediaBlocks) {
@@ -4930,6 +5038,40 @@ void MarkdownArticle::Impl::stopPlaceholderRipple(
requestPlaceholderRepaint(id);
}
void MarkdownArticle::Impl::addButtonRowRipple(
PreparedMediaBlockId id,
int index,
QPoint point) {
const auto block = FindButtonRowBlock(&_blocks, id);
if (!block) {
return;
}
auto runtime = block->buttonRowRuntime
? block->buttonRowRuntime
: getOrCreateButtonRowRuntime(id);
if (!runtime) {
return;
}
block->buttonRowRuntime = runtime;
AddButtonRowRipple(
runtime,
block->buttons,
index,
point,
layoutStyle().buttonRow);
}
void MarkdownArticle::Impl::stopButtonRowRipple(PreparedMediaBlockId id) {
if (!id) {
return;
}
const auto i = _buttonRowRuntimes.find(id.value);
if (i == end(_buttonRowRuntimes)) {
return;
}
StopButtonRowRipple(i->second);
}
void MarkdownArticle::Impl::invalidateLayout() {
invalidateLayout(true);
}
@@ -5007,6 +5149,10 @@ void MarkdownArticle::Impl::clearPlaceholderRuntimes() {
_placeholderRuntimes.clear();
}
void MarkdownArticle::Impl::clearButtonRowRuntimes() {
_buttonRowRuntimes.clear();
}
void MarkdownArticle::Impl::refreshMediaBlockHosts() {
for (const auto &[id, block] : _mediaBlocks) {
if (block) {
@@ -5047,6 +5193,23 @@ MarkdownArticle::Impl::getOrCreatePlaceholderRuntime(
return runtime;
}
auto MarkdownArticle::Impl::getOrCreateButtonRowRuntime(
PreparedMediaBlockId id)
-> std::shared_ptr<ButtonRowRuntime> {
if (!id) {
return nullptr;
}
if (const auto i = _buttonRowRuntimes.find(id.value);
i != end(_buttonRowRuntimes)) {
return i->second;
}
auto runtime = std::make_shared<ButtonRowRuntime>([=] {
requestButtonRowRepaint(id);
});
_buttonRowRuntimes.emplace(id.value, runtime);
return runtime;
}
void MarkdownArticle::Impl::pruneTaskMarkerRuntimes() {
auto live = TaskMarkerSourceSet();
CollectTaskMarkerSources(_blocks, &live);
@@ -5072,6 +5235,18 @@ void MarkdownArticle::Impl::prunePlaceholderRuntimes() {
}
}
void MarkdownArticle::Impl::pruneButtonRowRuntimes() {
auto live = std::unordered_set<uint64>();
CollectButtonRowIds(_blocks, &live);
for (auto i = _buttonRowRuntimes.begin(); i != _buttonRowRuntimes.end();) {
if (live.find(i->first) != end(live)) {
++i;
} else {
i = _buttonRowRuntimes.erase(i);
}
}
}
void MarkdownArticle::Impl::requestTaskMarkerRepaint(
const PreparedEditListItemSource &source) {
if (const auto block = FindListItemBlock(_blocks, source)) {
@@ -5099,6 +5274,18 @@ void MarkdownArticle::Impl::requestPlaceholderRepaint(
}
}
void MarkdownArticle::Impl::requestButtonRowRepaint(PreparedMediaBlockId id) {
if (const auto block = FindButtonRowBlock(_blocks, id)) {
if (_textRepaintRect) {
_textRepaintRect(block->outer);
} else if (_textRepaint) {
_textRepaint();
}
} else if (_textRepaint) {
_textRepaint();
}
}
std::shared_ptr<MediaBlock> MarkdownArticle::Impl::getOrCreateMediaBlock(
const PreparedBlock &prepared) {
switch (prepared.kind) {
@@ -5879,6 +6066,7 @@ void MarkdownArticle::Impl::finalizeRelayout(int heightBottom) {
page.left() + page.right() + 1));
pruneTaskMarkerRuntimes();
prunePlaceholderRuntimes();
pruneButtonRowRuntimes();
_relatedArticleImages.clear();
StoreRelatedArticleImageStates(
_blocks,
@@ -5968,6 +6156,9 @@ void MarkdownArticle::Impl::relayout(int width) {
context.placeholderRuntimeFactory = [=](PreparedPlaceholderBlockId id) {
return getOrCreatePlaceholderRuntime(id);
};
context.buttonRowRuntimeFactory = [=](PreparedMediaBlockId id) {
return getOrCreateButtonRowRuntime(id);
};
context.taskMarkerRippleRuntimeFactory
= [=](const PreparedEditListItemSource &source) {
return getOrCreateTaskMarkerRippleRuntime(source);
@@ -6051,6 +6242,9 @@ void MarkdownArticle::Impl::relayoutRetained(int width) {
context.placeholderRuntimeFactory = [=](PreparedPlaceholderBlockId id) {
return getOrCreatePlaceholderRuntime(id);
};
context.buttonRowRuntimeFactory = [=](PreparedMediaBlockId id) {
return getOrCreateButtonRowRuntime(id);
};
context.taskMarkerRippleRuntimeFactory
= [=](const PreparedEditListItemSource &source) {
return getOrCreateTaskMarkerRippleRuntime(source);
@@ -6514,6 +6708,17 @@ void MarkdownArticle::stopPlaceholderRipple(PreparedPlaceholderBlockId id) {
_impl->stopPlaceholderRipple(id);
}
void MarkdownArticle::addButtonRowRipple(
PreparedMediaBlockId id,
int index,
QPoint point) {
_impl->addButtonRowRipple(id, index, point);
}
void MarkdownArticle::stopButtonRowRipple(PreparedMediaBlockId id) {
_impl->stopButtonRowRipple(id);
}
void MarkdownArticle::clearBeforeDestroy() {
base::take(_impl);
}

View File

@@ -224,12 +224,20 @@ struct MarkdownArticlePaintContext final : Ui::ChatPaintContext {
}
};
struct MarkdownArticleButtonRowHit {
PreparedMediaBlockId id;
QPoint localPoint;
int index = -1;
};
struct MarkdownArticleHitTestResult {
int segmentIndex = -1;
Ui::Text::StateResult state;
std::optional<PreparedLink> preparedLink;
MediaActivation mediaActivation;
QPoint placeholderLocalPoint;
MarkdownArticleButtonRowHit buttonRow;
QString customTooltip;
int forcedOffset = -1;
bool direct = false;
bool codeHeaderCopy = false;
@@ -496,6 +504,11 @@ public:
void clearAllPlaceholderLoading();
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
void addButtonRowRipple(
PreparedMediaBlockId id,
int index,
QPoint point);
void stopButtonRowRipple(PreparedMediaBlockId id);
void clearBeforeDestroy();

View File

@@ -1282,6 +1282,7 @@ void CopyBlockCachedTextLeafs(
&block.placeholderLeaf);
break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::Quote:
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
@@ -2538,6 +2539,8 @@ int BlockSkip(
return skips.code;
case PreparedBlockKind::Rule:
return skips.rule;
case PreparedBlockKind::ButtonRow:
return skips.buttonRow;
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
return skips.paragraph;
@@ -2837,6 +2840,7 @@ void UpdateLaidOutLeafContent(
}
break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Quote:
@@ -2989,6 +2993,13 @@ void UpdateLaidOutLeafContent(
int top,
int width,
LayoutContext context);
[[nodiscard]] std::optional<int> LayoutButtonRowBlockGeometry(
const PreparedBlock &prepared,
LaidOutBlock *block,
const style::Markdown &st,
int left,
int top,
int width);
[[nodiscard]] std::optional<int> LayoutRelatedArticleBlockGeometry(
const PreparedBlock &prepared,
LaidOutBlock *block,
@@ -3439,6 +3450,56 @@ LaidOutBlock LayoutPlaceholderBlock(
return FinalizeLaidOutBlock(std::move(block));
}
LaidOutBlock LayoutButtonRowBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &st,
int left,
int top,
int width,
LayoutContext context) {
auto block = LaidOutBlock();
ApplyPreparedEditSources(&block, prepared);
block.kind = PreparedBlockKind::ButtonRow;
block.buttonRowId = prepared.buttonRow.id;
if (block.buttonRowId && context.buttonRowRuntimeFactory) {
block.buttonRowRuntime = context.buttonRowRuntimeFactory(
block.buttonRowId);
}
const auto &style = st.buttonRow;
block.buttons.reserve(prepared.buttonRow.buttons.size());
for (const auto &entry : prepared.buttonRow.buttons) {
auto button = LaidOutButton();
button.type = entry.button.type;
button.color = entry.button.visual.color;
SetTextLeaf(
&button.label,
style.labelStyle,
st,
entry.text,
formulas,
inlineFormulaObjects,
mediaRuntime,
PlainTextMinResizeWidth(style.labelStyle),
context.rtl,
context.repaint,
context.repaintRect);
button.fullLabel = button.label.toString();
block.buttons.push_back(std::move(button));
}
const auto bottom = LayoutButtonRowBlockGeometry(
prepared,
&block,
st,
left,
top,
width);
Expects(bottom.has_value());
return FinalizeLaidOutBlock(std::move(block));
}
LaidOutBlock LayoutRelatedArticleBlock(
const PreparedBlock &prepared,
const style::Markdown &st,
@@ -4535,6 +4596,51 @@ LaidOutBlock LayoutGroupedMediaBlock(
return block->outer.y() + block->outer.height();
}
// LayoutButtonRowButtons fills every button rect relative to the row origin,
// so the whole row is translated here by the block's top-left corner. From
// this point on the button rects live in exactly the same coordinate space
// as block->outer, which is what paint, hit testing and the ripple local
// point all assume, and what the retained relayout path keeps true when it
// re-runs this function at a different width.
[[nodiscard]] std::optional<int> LayoutButtonRowBlockGeometry(
const PreparedBlock &prepared,
LaidOutBlock *block,
const style::Markdown &st,
int left,
int top,
int width) {
if (!block) {
return std::nullopt;
}
ClearBlockGeometry(block);
const auto &style = st.buttonRow;
const auto blockWidth = std::max(width, 1);
LayoutButtonRowButtons(
&block->buttons,
prepared.flowAlignment,
blockWidth,
style);
const auto shift = QPoint(left, top);
for (auto &button : block->buttons) {
button.rect.translate(shift);
button.labelRect.translate(shift);
if (button.icon) {
button.iconRect.translate(shift);
}
button.logicalRect = button.rect;
button.logicalLabelRect = button.labelRect;
button.logicalIconRect = button.iconRect;
}
block->outer = QRect(left, top, blockWidth, style.height);
block->contentRect = block->outer;
RefreshButtonRowHandlers(
block->buttonRowRuntime,
prepared.buttonRow,
block->buttons);
FinishBlockGeometry(block);
return block->outer.y() + block->outer.height();
}
[[nodiscard]] std::optional<int> LayoutRelatedArticleBlockGeometry(
const PreparedBlock &prepared,
LaidOutBlock *block,
@@ -4831,6 +4937,14 @@ std::optional<int> RecountSimpleLaidOutBlock(
context);
case PreparedBlockKind::Rule:
return LayoutRuleBlockGeometry(block, st, left, top, width);
case PreparedBlockKind::ButtonRow:
return LayoutButtonRowBlockGeometry(
prepared,
block,
st,
left,
top,
width);
case PreparedBlockKind::DisplayMath:
return LayoutDisplayMathBlockGeometry(
prepared,

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "iv/markdown/iv_markdown_article.h"
#include "iv/markdown/iv_markdown_button_row.h"
#include "spellcheck/spellcheck_highlight_syntax.h"
#include <functional>
@@ -110,9 +111,11 @@ struct LaidOutBlock {
std::optional<PreparedLink> preparedLink;
ClickHandlerPtr preparedLinkHandler;
PreparedPlaceholderBlockId placeholderId;
PreparedMediaBlockId buttonRowId;
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0;
std::vector<LaidOutBlock> children;
std::vector<LaidOutTableRow> tableRows;
std::vector<LaidOutButton> buttons;
std::vector<int> tableColumnWidths;
QRect outer;
QRect headerRect;
@@ -178,6 +181,7 @@ struct LaidOutBlock {
std::optional<PreparedEditLeafSource> editLeaf;
std::shared_ptr<MediaBlock> mediaBlock;
std::shared_ptr<PlaceholderBlockRuntime> placeholderRuntime;
std::shared_ptr<ButtonRowRuntime> buttonRowRuntime;
std::shared_ptr<TaskMarkerRippleRuntime> taskMarkerRippleRuntime;
std::shared_ptr<PhotoRuntime> photoRuntime;
MediaActivation activation;
@@ -336,6 +340,8 @@ struct LayoutContext {
std::function<std::shared_ptr<MediaBlock>(const PreparedBlock&)> mediaBlockFactory;
std::function<std::shared_ptr<PlaceholderBlockRuntime>(
PreparedPlaceholderBlockId)> placeholderRuntimeFactory;
std::function<std::shared_ptr<ButtonRowRuntime>(
PreparedMediaBlockId)> buttonRowRuntimeFactory;
std::function<std::shared_ptr<TaskMarkerRippleRuntime>(
const PreparedEditListItemSource&)> taskMarkerRippleRuntimeFactory;
};
@@ -646,6 +652,16 @@ void UpdateLaidOutLeafContent(
int top,
int width,
LayoutContext context = {});
[[nodiscard]] LaidOutBlock LayoutButtonRowBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &st,
int left,
int top,
int width,
LayoutContext context = {});
[[nodiscard]] LaidOutBlock LayoutRelatedArticleBlock(
const PreparedBlock &prepared,
const style::Markdown &st,

View File

@@ -58,6 +58,7 @@ namespace {
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Placeholder:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::EmbedPost:
return false;
}
@@ -283,6 +284,7 @@ void PrepareNestedContext(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Photo:
@@ -477,6 +479,7 @@ void FinalizeOwnerSelection(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Photo:
@@ -1179,6 +1182,19 @@ void FinalizeOwnerSelection(
contentOverhead + analysis.contentPreferredWidth);
analysis.ownerEligible = !prepared.children.empty();
} break;
case PreparedBlockKind::ButtonRow: {
const auto minimum = std::max(
ButtonRowMinWidth(
int(prepared.buttonRow.buttons.size()),
st.buttonRow),
1);
analysis.contentMinimumWidth = minimum;
analysis.contentPreferredWidth = minimum;
analysis.outerMinimumWidth = minimum;
analysis.outerPreferredWidth = minimum;
analysis.scrollOwnerMinimumWidth = minimum;
analysis.ownerEligible = false;
} break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
@@ -1832,6 +1848,19 @@ void FinalizeOwnerSelection(
contentOverhead + analysis.contentPreferredWidth);
analysis.ownerEligible = !prepared.children.empty();
} break;
case PreparedBlockKind::ButtonRow: {
const auto minimum = std::max(
ButtonRowMinWidth(
int(prepared.buttonRow.buttons.size()),
st.buttonRow),
1);
analysis.contentMinimumWidth = minimum;
analysis.contentPreferredWidth = minimum;
analysis.outerMinimumWidth = minimum;
analysis.outerPreferredWidth = minimum;
analysis.scrollOwnerMinimumWidth = minimum;
analysis.ownerEligible = false;
} break;
case PreparedBlockKind::Rule:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
@@ -2433,6 +2462,17 @@ using LayoutListChildCallback = std::function<std::optional<int>(
context);
case PreparedBlockKind::Rule:
return LayoutRuleBlock(prepared, st, left, top, width);
case PreparedBlockKind::ButtonRow:
return LayoutButtonRowBlock(
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
st,
left,
top,
width,
context);
case PreparedBlockKind::List:
return LayoutListBlock(
prepared,
@@ -3624,6 +3664,7 @@ int LayoutBlocks(
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Photo:
@@ -3823,6 +3864,8 @@ int LayoutBlocks(
outerRight);
case PreparedBlockKind::Rule:
return block.outer.x();
case PreparedBlockKind::ButtonRow:
return outerRight;
case PreparedBlockKind::DisplayMath: {
if (!block.textRect.isEmpty()) {
return outerRight;

View File

@@ -308,6 +308,7 @@ void PaintSelectableTextLeaf(
block.textRect,
block.textWidth);
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
return CountGenericRevealBand(block.outer);
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
@@ -1453,6 +1454,7 @@ void PaintTableCaption(
case PreparedBlockKind::Thinking:
case PreparedBlockKind::Heading:
case PreparedBlockKind::Rule:
case PreparedBlockKind::ButtonRow:
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Photo:
@@ -3061,6 +3063,15 @@ void PaintBlock(
p.fillRect(block.outer, EffectiveDividerFg(paintSt, context));
});
break;
case PreparedBlockKind::ButtonRow:
PaintRevealBand(
p,
context,
block.outer,
[&](Painter &p, const MarkdownArticlePaintContext &context) {
PaintButtonRow(p, block, st, context, outerWidth);
});
break;
case PreparedBlockKind::List:
{
const auto childContext = block.scrollViewportRect.isEmpty()

View File

@@ -107,6 +107,9 @@ void RefreshBlockSegmentRect(
case SelectableSegmentKind::Media:
segment->outerRect = block.visibleMediaRect;
break;
case SelectableSegmentKind::ButtonRow:
segment->outerRect = block.outer;
break;
case SelectableSegmentKind::CodeBlock:
segment->outerRect = block.outer;
segment->textRect = VisibleTextRect(
@@ -321,6 +324,21 @@ void RefreshBlockSegmentRect(
block.leaf);
}
[[nodiscard]] TextForMimeData CopyTextForButtonRowBlock(
const LaidOutBlock &block) {
auto result = TextForMimeData();
for (const auto &button : block.buttons) {
if (button.label.isEmpty()) {
continue;
}
if (!result.empty()) {
result.append(u" "_q);
}
result.append(button.label.toTextForMimeData());
}
return result;
}
[[nodiscard]] int AddSelectableSegment(
std::vector<SelectableSegment> *segments,
SelectableSegment segment) {
@@ -704,6 +722,7 @@ void ApplyRichPageSliceEndTrim(TextWithEntities *target, int offset) {
case SelectableSegmentKind::Placeholder:
case SelectableSegmentKind::Photo:
case SelectableSegmentKind::Media:
case SelectableSegmentKind::ButtonRow:
return BlockBelongsToStructuralSelection(
*segment.block,
*selectionState.structuralSelection);
@@ -1025,6 +1044,16 @@ void CollectSelectableSegments(
}
continue;
}
case PreparedBlockKind::ButtonRow: {
auto segment = SelectableSegment();
segment.kind = SelectableSegmentKind::ButtonRow;
segment.block = &block;
segment.outerRect = block.outer;
segment.length = 1;
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
} break;
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Quote:
@@ -1434,6 +1463,10 @@ TextForMimeData TextForSegment(
return segment.block
? CopyTextForSingleMediaBlock(*segment.block)
: TextForMimeData();
case SelectableSegmentKind::ButtonRow:
return segment.block
? CopyTextForButtonRowBlock(*segment.block)
: TextForMimeData();
}
return TextForMimeData();
}

View File

@@ -20,6 +20,7 @@ enum class SelectableSegmentKind {
Placeholder,
Photo,
Media,
ButtonRow,
};
struct SelectableSegment {

View File

@@ -0,0 +1,606 @@
/*
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 "ui/effects/animation_value.h"
#include "ui/style/style_core_scale.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;
[[nodiscard]] const style::icon *ButtonRowIcon(ButtonType type) {
switch (type) {
case ButtonType::Url:
case ButtonType::Auth: return &st::msgBotKbUrlIcon;
case ButtonType::Buy: return &st::msgBotKbPaymentIcon;
case ButtonType::SwitchInline:
case ButtonType::SwitchInlineSame: return &st::msgBotKbSwitchPmIcon;
case ButtonType::WebView:
case ButtonType::SimpleWebView: return &st::msgBotKbWebviewIcon;
case ButtonType::CopyText: return &st::msgBotKbCopyIcon;
}
return nullptr;
}
[[nodiscard]] const HistoryMessageMarkupButton *LookupRuntimeButton(
const std::weak_ptr<ButtonRowRuntime> &weak,
int index) {
const auto strong = weak.lock();
return (strong && (index >= 0) && (index < int(strong->shared.size())))
? strong->shared[index]
: nullptr;
}
class RichPageButtonClickHandler final : public ClickHandler {
public:
RichPageButtonClickHandler(
std::weak_ptr<ButtonRowRuntime> runtime,
int index);
void onClick(ClickContext context) 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 strong = _runtime.lock();
if (!strong || (_index < 0) || (_index >= int(strong->shared.size()))) {
return;
}
const auto record = strong->shared[_index];
if (!record || !strong->page) {
return;
}
const auto my = context.other.value<ClickHandlerContext>();
Api::ActivateBotButton(my, [record, keepAlive = strong->page] {
return record;
});
}
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 = ButtonRowIcon(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 = ButtonRowIcon(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();
}
struct ButtonRowColors {
QColor bg;
QColor ripple;
QColor fg;
bool punchOut = false;
};
[[nodiscard]] ButtonRowColors ResolveButtonColors(
HistoryMessageMarkupButton::Color color,
const style::MarkdownButtonRow &st) {
using Color = HistoryMessageMarkupButton::Color;
switch (color) {
case Color::Primary:
return {
.bg = st.primaryBg->c,
.ripple = st.primaryRipple->c,
.punchOut = true,
};
case Color::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 Color::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(
Painter &p,
const LaidOutButton &button,
const MarkdownArticlePaintContext &context) {
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 = context.clip,
.palette = &p.textPalette(),
.now = context.now,
.elisionLines = 1,
});
}
void PaintButtonContent(
Painter &p,
const LaidOutButton &button,
QColor fg,
const MarkdownArticlePaintContext &context,
int outerWidth) {
if (button.icon) {
button.icon->paint(p, button.iconRect.topLeft(), outerWidth, fg);
}
p.setPen(fg);
PaintButtonLabel(p, button, context);
}
void PaintButtonPill(
Painter &p,
const LaidOutButton &button,
const ButtonRowColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
int outerWidth) {
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) {
ripple->paint(
p,
button.rect.x(),
button.rect.y(),
outerWidth,
&colors.ripple);
}
}
void PaintPlainButton(
Painter &p,
const LaidOutButton &button,
const ButtonRowColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
const MarkdownArticlePaintContext &context,
int outerWidth,
bool disabled) {
PaintButtonPill(p, button, colors, ripple, st, outerWidth);
const auto was = p.opacity();
if (disabled) {
p.setOpacity(was * st.disabledOpacity);
}
PaintButtonContent(p, button, colors.fg, context, outerWidth);
if (disabled) {
p.setOpacity(was);
}
}
// A primary pill is composed offscreen so that its label glyphs, its
// text-colored custom emoji and its corner icon can be erased out of the
// accent fill with CompositionMode_DestinationOut. No code here reads the
// surface behind the row: 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 label is then drawn
// once more on the real painter with a fully transparent pen: 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 PaintPrimaryButton(
Painter &p,
const LaidOutButton &button,
const ButtonRowColors &colors,
Ui::RippleAnimation *ripple,
const style::MarkdownButtonRow &st,
const MarkdownArticlePaintContext &context,
int outerWidth,
bool disabled) {
const auto ratio = style::DevicePixelRatio();
const auto opacity = disabled ? st.disabledPrimaryOpacity : 1.;
auto frame = QImage(
button.rect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
frame.setDevicePixelRatio(ratio);
frame.fill(Qt::transparent);
{
auto q = Painter(&frame);
q.translate(-button.rect.topLeft());
q.setTextPalette(p.textPalette());
PaintButtonPill(q, button, colors, ripple, st, outerWidth);
q.setCompositionMode(QPainter::CompositionMode_DestinationOut);
q.setOpacity(opacity);
PaintButtonContent(q, button, QColor(Qt::white), context, outerWidth);
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
}
p.drawImage(button.rect.topLeft(), frame);
const auto was = p.opacity();
p.setOpacity(was * opacity);
p.setPen(QColor(Qt::transparent));
PaintButtonLabel(p, button, context);
p.setOpacity(was);
}
} // namespace
ButtonRowRuntime::ButtonRowRuntime(Fn<void()> repaint)
: repaint(std::move(repaint)) {
}
int ButtonRowMinWidth(int count, const style::MarkdownButtonRow &st) {
return (count > 0)
? (count * st.height + (count - 1) * st.spacing)
: 0;
}
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->page = prepared.page;
runtime->shared.resize(count);
for (auto i = 0; i != count; ++i) {
runtime->shared[i] = list[i].shared;
}
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;
}
}
void PaintButtonRow(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &st,
const MarkdownArticlePaintContext &context,
int outerWidth) {
const auto &style = context.paintMarkdownStyle(st).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 = ResolveButtonColors(button.color, style);
const auto disabled = (button.type == ButtonType::Disabled);
const auto ripple = (runtime
&& runtime->ripple
&& (runtime->rippleIndex == i))
? runtime->ripple.get()
: nullptr;
if (colors.punchOut) {
PaintPrimaryButton(
p,
button,
colors,
ripple,
style,
context,
outerWidth,
disabled);
} else {
PaintPlainButton(
p,
button,
colors,
ripple,
style,
context,
outerWidth,
disabled);
}
}
}
void AddButtonRowRipple(
const std::shared_ptr<ButtonRowRuntime> &runtime,
const std::vector<LaidOutButton> &buttons,
int index,
QPoint point,
const style::MarkdownButtonRow &st) {
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;
}
const auto repaint = runtime->repaint;
if (!runtime->ripple
|| (runtime->rippleIndex != index)
|| (runtime->rippleSize != size)) {
runtime->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(size, st.height / 2),
[=] {
if (repaint) {
repaint();
}
});
runtime->rippleSize = size;
runtime->rippleIndex = index;
}
point.setX(std::clamp(point.x(), 0, std::max(size.width() - 1, 0)));
point.setY(std::clamp(point.y(), 0, std::max(size.height() - 1, 0)));
runtime->ripple->add(point);
if (repaint) {
repaint();
}
}
void StopButtonRowRipple(const std::shared_ptr<ButtonRowRuntime> &runtime) {
if (!runtime || !runtime->ripple) {
return;
}
runtime->ripple->lastStop();
if (runtime->repaint) {
runtime->repaint();
}
}
} // namespace Iv::Markdown

View File

@@ -0,0 +1,89 @@
/*
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
*/
#pragma once
#include "iv/markdown/iv_markdown_prepare.h"
#include "ui/effects/ripple_animation.h"
#include "ui/style/style_core_types.h"
#include "ui/text/text.h"
#include "ui/click_handler.h"
#include "ui/painter.h"
#include <memory>
#include <vector>
namespace style {
struct Markdown;
struct MarkdownButtonRow;
} // namespace style
namespace Iv::Markdown {
struct LaidOutBlock;
struct MarkdownArticlePaintContext;
struct LaidOutButton {
Ui::Text::String label;
QString fullLabel;
QRect rect;
QRect labelRect;
QRect iconRect;
QRect logicalRect;
QRect logicalLabelRect;
QRect logicalIconRect;
const style::icon *icon = nullptr;
HistoryMessageMarkupButton::Type type
= HistoryMessageMarkupButton::Type::Disabled;
HistoryMessageMarkupButton::Color color
= HistoryMessageMarkupButton::Color::Normal;
bool elided = false;
};
struct ButtonRowRuntime {
explicit ButtonRowRuntime(Fn<void()> repaint);
Fn<void()> repaint;
std::shared_ptr<const RichPage> page;
std::vector<const HistoryMessageMarkupButton*> shared;
std::vector<ClickHandlerPtr> handlers;
std::unique_ptr<Ui::RippleAnimation> ripple;
QSize rippleSize;
int rippleIndex = -1;
};
[[nodiscard]] int ButtonRowMinWidth(
int count,
const style::MarkdownButtonRow &st);
void LayoutButtonRowButtons(
std::vector<LaidOutButton> *buttons,
TableAlignment alignment,
int width,
const style::MarkdownButtonRow &st);
[[nodiscard]] int ButtonRowHitIndex(
const std::vector<LaidOutButton> &buttons,
QPoint point);
void RefreshButtonRowHandlers(
const std::shared_ptr<ButtonRowRuntime> &runtime,
const PreparedButtonRowBlockData &prepared,
const std::vector<LaidOutButton> &buttons);
void PaintButtonRow(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &st,
const MarkdownArticlePaintContext &context,
int outerWidth);
void AddButtonRowRipple(
const std::shared_ptr<ButtonRowRuntime> &runtime,
const std::vector<LaidOutButton> &buttons,
int index,
QPoint point,
const style::MarkdownButtonRow &st);
void StopButtonRowRipple(const std::shared_ptr<ButtonRowRuntime> &runtime);
} // namespace Iv::Markdown

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "history/history_item_reply_markup.h"
#include "iv/markdown/iv_markdown_document.h"
#include "iv/markdown/iv_markdown_math_renderer.h"
@@ -35,6 +36,7 @@ enum class PreparedBlockKind {
Heading,
CodeBlock,
Rule,
ButtonRow,
List,
ListItem,
Quote,
@@ -651,6 +653,21 @@ struct PreparedPlaceholderBlockData {
std::optional<EmbedRequest> embed;
};
struct PreparedButtonRowButton {
TextWithEntities text;
HistoryMessageMarkupButton button = HistoryMessageMarkupButton(
HistoryMessageMarkupButton::Type::Disabled,
QString(),
{});
const HistoryMessageMarkupButton *shared = nullptr;
};
struct PreparedButtonRowBlockData {
PreparedMediaBlockId id;
std::shared_ptr<const Iv::RichPage> page;
std::vector<PreparedButtonRowButton> buttons;
};
struct PreparedRelatedArticleBlockData {
PreparedLink link;
QString copyText;
@@ -687,6 +704,7 @@ struct PreparedBlock {
PreparedGroupedMediaBlockData groupedMedia;
PreparedEmbedPostBlockData embedPost;
PreparedPlaceholderBlockData placeholder;
PreparedButtonRowBlockData buttonRow;
PreparedRelatedArticleBlockData relatedArticle;
ListKind listKind = ListKind::Bullet;
ListDelimiter listDelimiter = ListDelimiter::None;

View File

@@ -1916,6 +1916,8 @@ void ClearPreparedEditSources(std::vector<PreparedBlock> *blocks) {
false,
std::move(editBlock));
}
case RichPageBlockKind::ButtonRow:
return PrepareNativeIvButtonRowBlock(block, result, state);
case RichPageBlockKind::List: {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::List;

View File

@@ -357,6 +357,32 @@ void ApplyEmptyMediaCaptionPlaceholder(
block->editPlaceholderText = tr::lng_photo_caption(tr::now);
}
[[nodiscard]] TableAlignment NativeIvButtonRowAlignment(
Iv::RichPage::ButtonAlignment alignment) {
using Alignment = Iv::RichPage::ButtonAlignment;
switch (alignment) {
case Alignment::Stretch: return TableAlignment::None;
case Alignment::Left: return TableAlignment::Left;
case Alignment::Center: return TableAlignment::Center;
case Alignment::Right: return TableAlignment::Right;
}
Unexpected("Alignment in NativeIvButtonRowAlignment.");
}
[[nodiscard]] TextWithEntities NormalizeNativeIvButtonLabel(
TextWithEntities text) {
ExpandInlineTextObjects(&text, false);
text.entities.erase(
ranges::remove_if(text.entities, [](const EntityInText &entity) {
const auto type = entity.type();
return (type != EntityType::CustomEmoji)
&& (type != EntityType::FormattedDate);
}),
text.entities.end());
TextUtilities::Trim(text);
return text;
}
} // namespace
bool PrepareNativeIvRichText(
@@ -652,4 +678,28 @@ bool PrepareNativeIvGroupedMediaBlock(
return true;
}
bool PrepareNativeIvButtonRowBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
if (data.buttons.empty()) {
return true;
}
auto block = PreparedBlock();
block.kind = PreparedBlockKind::ButtonRow;
block.flowAlignment = NativeIvButtonRowAlignment(data.buttonAlignment);
block.buttonRow.id = GeneratePreparedMediaBlockId(state);
block.buttonRow.page = state->result.richPage;
block.buttonRow.buttons.reserve(data.buttons.size());
for (const auto &button : data.buttons) {
block.buttonRow.buttons.push_back({
.text = NormalizeNativeIvButtonLabel(button.text.text),
.button = button.button,
.shared = &button.button,
});
}
result->push_back(std::move(block));
return true;
}
} // namespace Iv::Markdown

View File

@@ -57,6 +57,10 @@ struct NativeIvRichTextContext {
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvButtonRowBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvRichText(
const Iv::RichPage::RichText &text,
PreparedIvRichText *result,

View File

@@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/markdown/iv_markdown_prepare_serialize.h"
#include "base/variant.h"
#include "ui/text/text_utilities.h"
#include "styles/style_iv.h"
#include <QtCore/QByteArray>
@@ -105,6 +108,55 @@ std::optional<InlineTextObjectEntity> ParseInlineTextObjectEntity(
return std::nullopt;
}
void ExpandInlineTextObjects(TextWithEntities *text, bool withIcons) {
auto &entities = text->entities;
for (auto i = entities.begin(); i != entities.end();) {
if (i->type() != EntityType::CustomEmoji) {
++i;
continue;
}
const auto object = ParseInlineTextObjectEntity(i->data());
if (!object) {
++i;
continue;
}
const auto replacement = v::match(object->data, [](
const InlineTextObjectFormulaData &data) {
return data.trimmedTex;
}, [](const InlineTextObjectIvImageData &data) {
return data.replacementText;
});
const auto offset = i->offset();
const auto length = i->length();
const auto delta = int(replacement.size()) - length;
text->text.replace(offset, length, replacement);
for (auto &entity : entities) {
if (&entity == &*i) {
continue;
} else if (entity.offset() > offset) {
entity.shiftRight(delta);
} else if (entity.offset() + entity.length() > offset) {
entity.shrinkFromRight(-delta);
}
}
const auto formula = (object->kind
== InlineTextObjectKind::Formula);
if (withIcons && formula && !replacement.isEmpty()) {
const auto icon = Ui::Text::IconEmoji(
&st::ivSummaryMathIcon,
replacement);
*i = EntityInText(
EntityType::CustomEmoji,
offset,
int(replacement.size()),
icon.entities.front().data());
++i;
} else {
i = entities.erase(i);
}
}
}
QString InlineFormulaCopySource(const QString &source) {
return u"$"_q + source + u"$"_q;
}

View File

@@ -19,6 +19,7 @@ namespace Iv::Markdown {
const InlineTextObjectEntity &object);
[[nodiscard]] std::optional<InlineTextObjectEntity> ParseInlineTextObjectEntity(
QStringView data);
void ExpandInlineTextObjects(TextWithEntities *text, bool withIcons);
[[nodiscard]] QString InlineFormulaCopySource(const QString &source);
[[nodiscard]] MarkdownPrepareDimensions CaptureMarkdownPrepareDimensions();
[[nodiscard]] MarkdownPrepareDimensions CaptureMarkdownPrepareDimensions(

View File

@@ -278,6 +278,7 @@ void MarkdownDocumentWidget::articleContentChanged() {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
stopPressedPlaceholderRipple();
stopPressedButtonRowRipple();
clearSelection();
_articlePainted = false;
resetTextPaintCaches();
@@ -841,6 +842,7 @@ void MarkdownDocumentWidget::mouseDoubleClickEvent(QMouseEvent *e) {
void MarkdownDocumentWidget::focusOutEvent(QFocusEvent *e) {
stopPressedPlaceholderRipple();
stopPressedButtonRowRipple();
if (!_selection.empty()) {
_savedSelection = _selection;
_savedSelectionEndpoints = _selectionEndpoints;
@@ -883,6 +885,7 @@ bool MarkdownDocumentWidget::eventHook(QEvent *e) {
void MarkdownDocumentWidget::leaveEventHook(QEvent *e) {
ClickHandler::clearActive(this);
_hoverTooltip = QString();
Ui::Tooltip::Hide();
applyCursor((_dragAction == Selecting)
? style::cur_text
@@ -910,9 +913,11 @@ void MarkdownDocumentWidget::clickHandlerPressedChanged(
QString MarkdownDocumentWidget::tooltipText() const {
if (const auto lnk = ClickHandler::getActive()) {
return lnk->tooltip();
if (const auto text = lnk->tooltip(); !text.isEmpty()) {
return text;
}
}
return QString();
return _hoverTooltip;
}
QPoint MarkdownDocumentWidget::tooltipPos() const {
@@ -1127,10 +1132,13 @@ void MarkdownDocumentWidget::forceRelayoutCurrentWidth() {
void MarkdownDocumentWidget::updateHover(
const MarkdownArticleHitTestResult &state) {
const auto changed = ClickHandler::setActive(state.state.link, this);
if (changed) {
const auto tooltipChanged = (_hoverTooltip != state.customTooltip);
_hoverTooltip = state.customTooltip;
if (changed || tooltipChanged) {
Ui::Tooltip::Hide();
}
if (state.state.link && _dragAction == NoDrag) {
if ((state.state.link || !_hoverTooltip.isEmpty())
&& _dragAction == NoDrag) {
Ui::Tooltip::Show(1000, this);
}
auto cursor = style::cur_default;
@@ -1187,6 +1195,7 @@ void MarkdownDocumentWidget::updateHoverAtCursor() {
| Ui::Text::StateRequest::Flag::LookupSymbol));
} else {
ClickHandler::clearActive(this);
_hoverTooltip = QString();
applyCursor(style::cur_default);
}
}
@@ -1300,10 +1309,20 @@ void MarkdownDocumentWidget::stopPressedPlaceholderRipple() {
}
}
void MarkdownDocumentWidget::stopPressedButtonRowRipple() {
if (_pressedButtonRow.index >= 0) {
if (_article) {
_article->stopButtonRowRipple(_pressedButtonRow.id);
}
_pressedButtonRow = {};
}
}
void MarkdownDocumentWidget::dragActionStart(
QPoint point,
Qt::MouseButton button) {
stopPressedPlaceholderRipple();
stopPressedButtonRowRipple();
const auto state = hitTest(
point,
Ui::Text::StateRequest::Flag::LookupLink
@@ -1326,6 +1345,13 @@ void MarkdownDocumentWidget::dragActionStart(
state.mediaActivation.placeholderId,
state.placeholderLocalPoint);
}
if ((state.buttonRow.index >= 0) && _article) {
_pressedButtonRow = state.buttonRow;
_article->addButtonRowRipple(
state.buttonRow.id,
state.buttonRow.index,
state.buttonRow.localPoint);
}
_dragStartPosition = point;
_dragStartHadSelection = !selectionForCopy().empty();
_selectionClickPreparedLink = (state.preparedLink
@@ -1402,6 +1428,7 @@ MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
Qt::MouseButton button) {
const auto state = dragActionUpdate(point);
stopPressedPlaceholderRipple();
stopPressedButtonRowRipple();
auto activated = ClickHandler::unpressed();
const auto dragStartHadSelection = _dragStartHadSelection;
const auto wasClick = (_dragAction == NoDrag)

View File

@@ -148,6 +148,7 @@ private:
[[nodiscard]] MarkdownArticlePaintContext textPaintContext(QRect clip);
void touchEvent(QTouchEvent *e);
void stopPressedPlaceholderRipple();
void stopPressedButtonRowRipple();
void dragActionStart(QPoint point, Qt::MouseButton button);
MarkdownArticleHitTestResult dragActionUpdate(QPoint point);
MarkdownArticleHitTestResult dragActionFinish(
@@ -185,6 +186,8 @@ private:
base::Timer _tripleClickTimer;
std::optional<PreparedLink> _selectionClickPreparedLink;
PreparedPlaceholderBlockId _pressedPlaceholderId;
MarkdownArticleButtonRowHit _pressedButtonRow;
QString _hoverTooltip;
bool _dragStartHadSelection = false;
int _lastRelayoutMs = 0;
int _zoom = 100;

View File

@@ -72,6 +72,8 @@ PRIVATE
iv/markdown/iv_markdown_article_selection.h
iv/markdown/iv_markdown_article_text.cpp
iv/markdown/iv_markdown_article_text.h
iv/markdown/iv_markdown_button_row.cpp
iv/markdown/iv_markdown_button_row.h
iv/markdown/iv_markdown_controller.cpp
iv/markdown/iv_markdown_controller.h
iv/markdown/iv_markdown_document.cpp