mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
Show button rows in rich messages
Task: 2026/07/27/show-block-button-rows-in-rich-messages
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &,
|
||||
|
||||
@@ -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 █
|
||||
}
|
||||
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 █
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = █
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ enum class SelectableSegmentKind {
|
||||
Placeholder,
|
||||
Photo,
|
||||
Media,
|
||||
ButtonRow,
|
||||
};
|
||||
|
||||
struct SelectableSegment {
|
||||
|
||||
606
Telegram/SourceFiles/iv/markdown/iv_markdown_button_row.cpp
Normal file
606
Telegram/SourceFiles/iv/markdown/iv_markdown_button_row.cpp
Normal 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
|
||||
89
Telegram/SourceFiles/iv/markdown/iv_markdown_button_row.h
Normal file
89
Telegram/SourceFiles/iv/markdown/iv_markdown_button_row.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user