Support second button in web apps.

This commit is contained in:
John Preston 2024-09-09 13:29:00 +04:00
parent 49ee7ee52b
commit a35092f012
5 changed files with 258 additions and 96 deletions

View file

@ -141,10 +141,12 @@ paymentsLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
} }
botWebViewPanelSize: size(384px, 694px); botWebViewPanelSize: size(384px, 694px);
botWebViewBottomPadding: margins(12px, 12px, 12px, 12px);
botWebViewBottomSkip: point(12px, 8px);
botWebViewBottomButton: RoundButton(paymentsPanelSubmit) { botWebViewBottomButton: RoundButton(paymentsPanelSubmit) {
height: 56px; height: 40px;
style: TextStyle(defaultTextStyle) { style: TextStyle(defaultTextStyle) {
font: boxButtonFont; font: boxButtonFont;
} }
textTop: 19px; textTop: 11px;
} }

View file

@ -48,6 +48,26 @@ constexpr auto kProgressOpacity = 0.3;
constexpr auto kLightnessThreshold = 128; constexpr auto kLightnessThreshold = 128;
constexpr auto kLightnessDelta = 32; constexpr auto kLightnessDelta = 32;
struct ButtonArgs {
bool isActive = false;
bool isVisible = false;
bool isProgressVisible = false;
QString text;
};
[[nodiscard]] RectPart ParsePosition(const QString &position) {
if (position == u"left"_q) {
return RectPart::Left;
} else if (position == u"top"_q) {
return RectPart::Top;
} else if (position == u"right"_q) {
return RectPart::Right;
} else if (position == u"bottom"_q) {
return RectPart::Bottom;
}
return RectPart::Left;
}
[[nodiscard]] QJsonObject ParseMethodArgs(const QString &json) { [[nodiscard]] QJsonObject ParseMethodArgs(const QString &json) {
if (json.isEmpty()) { if (json.isEmpty()) {
return {}; return {};
@ -99,6 +119,15 @@ constexpr auto kLightnessDelta = 32;
alpha); alpha);
} }
[[nodiscard]] const style::color *LookupNamedColor(const QString &key) {
if (key == u"secondary_bg_color"_q) {
return &st::boxDividerBg;
} else if (key == u"bottom_bar_bg_color"_q) {
return &st::windowBg;
}
return nullptr;
}
} // namespace } // namespace
class Panel::Button final : public RippleButton { class Panel::Button final : public RippleButton {
@ -107,8 +136,11 @@ public:
~Button(); ~Button();
void updateBg(QColor bg); void updateBg(QColor bg);
void updateBg(not_null<const style::color*> paletteBg);
void updateFg(QColor fg); void updateFg(QColor fg);
void updateArgs(MainButtonArgs &&args); void updateFg(not_null<const style::color*> paletteFg);
void updateArgs(ButtonArgs &&args);
private: private:
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
@ -128,6 +160,9 @@ private:
style::owned_color _bg; style::owned_color _bg;
RoundRect _roundRect; RoundRect _roundRect;
rpl::lifetime _bgLifetime;
rpl::lifetime _fgLifetime;
}; };
struct Panel::Progress { struct Panel::Progress {
@ -171,15 +206,33 @@ Panel::Button::~Button() = default;
void Panel::Button::updateBg(QColor bg) { void Panel::Button::updateBg(QColor bg) {
_bg.update(bg); _bg.update(bg);
_roundRect.setColor(_bg.color()); _roundRect.setColor(_bg.color());
_bgLifetime.destroy();
update(); update();
} }
void Panel::Button::updateBg(not_null<const style::color*> paletteBg) {
updateBg((*paletteBg)->c);
_bgLifetime = style::PaletteChanged(
) | rpl::start_with_next([=] {
updateBg((*paletteBg)->c);
});
}
void Panel::Button::updateFg(QColor fg) { void Panel::Button::updateFg(QColor fg) {
_fg = fg; _fg = fg;
_fgLifetime.destroy();
update(); update();
} }
void Panel::Button::updateArgs(MainButtonArgs &&args) { void Panel::Button::updateFg(not_null<const style::color*> paletteFg) {
updateFg((*paletteFg)->c);
_fgLifetime = style::PaletteChanged(
) | rpl::start_with_next([=] {
updateFg((*paletteFg)->c);
});
}
void Panel::Button::updateArgs(ButtonArgs &&args) {
_textFull = std::move(args.text); _textFull = std::move(args.text);
setDisabled(!args.isActive); setDisabled(!args.isActive);
setPointerCursor(false); setPointerCursor(false);
@ -266,10 +319,7 @@ void Panel::Button::setupProgressGeometry() {
void Panel::Button::paintEvent(QPaintEvent *e) { void Panel::Button::paintEvent(QPaintEvent *e) {
Painter p(this); Painter p(this);
_roundRect.paintSomeRounded( _roundRect.paint(p, rect());
p,
rect().marginsAdded({ 0, st::callRadius * 2, 0, 0 }),
RectPart::BottomLeft | RectPart::BottomRight);
if (!isDisabled()) { if (!isDisabled()) {
const auto ripple = ResolveRipple(_bg.color()->c); const auto ripple = ResolveRipple(_bg.color()->c);
@ -292,12 +342,7 @@ void Panel::Button::paintEvent(QPaintEvent *e) {
} }
QImage Panel::Button::prepareRippleMask() const { QImage Panel::Button::prepareRippleMask() const {
return RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) { return RippleAnimation::RoundRectMask(size(), st::callRadius);
p.drawRoundedRect(
rect().marginsAdded({ 0, st::callRadius * 2, 0, 0 }),
st::callRadius,
st::callRadius);
});
} }
QPoint Panel::Button::prepareRippleStartPosition() const { QPoint Panel::Button::prepareRippleStartPosition() const {
@ -599,7 +644,7 @@ void Panel::createWebviewBottom() {
) | rpl::start_with_next([=](QRect inner, int height) { ) | rpl::start_with_next([=](QRect inner, int height) {
bottom->move(inner.x(), inner.y() + inner.height() - height); bottom->move(inner.x(), inner.y() + inner.height() - height);
bottom->resizeToWidth(inner.width()); bottom->resizeToWidth(inner.width());
updateFooterHeight(); layoutButtons();
}, bottom->lifetime()); }, bottom->lifetime());
} }
@ -633,7 +678,9 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
} }
if (_webviewBottom.get() == bottom) { if (_webviewBottom.get() == bottom) {
_webviewBottom = nullptr; _webviewBottom = nullptr;
_secondaryButton = nullptr;
_mainButton = nullptr; _mainButton = nullptr;
_bottomButtonsBg = nullptr;
} }
}); });
if (!raw->widget()) { if (!raw->widget()) {
@ -655,7 +702,6 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
}); });
}); });
updateFooterHeight();
rpl::combine( rpl::combine(
container->geometryValue(), container->geometryValue(),
_footerHeight.value() _footerHeight.value()
@ -681,7 +727,9 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
} else if (command == "web_app_switch_inline_query") { } else if (command == "web_app_switch_inline_query") {
switchInlineQueryMessage(arguments); switchInlineQueryMessage(arguments);
} else if (command == "web_app_setup_main_button") { } else if (command == "web_app_setup_main_button") {
processMainButtonMessage(arguments); processButtonMessage(_mainButton, arguments);
} else if (command == "web_app_setup_secondary_button") {
processButtonMessage(_secondaryButton, arguments);
} else if (command == "web_app_setup_back_button") { } else if (command == "web_app_setup_back_button") {
processBackButtonMessage(arguments); processBackButtonMessage(arguments);
} else if (command == "web_app_setup_settings_button") { } else if (command == "web_app_setup_settings_button") {
@ -714,6 +762,8 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
requestClipboardText(arguments); requestClipboardText(arguments);
} else if (command == "web_app_set_header_color") { } else if (command == "web_app_set_header_color") {
processHeaderColor(arguments); processHeaderColor(arguments);
} else if (command == "web_app_set_bottom_bar_color") {
processBottomBarColor(arguments);
} else if (command == "share_score") { } else if (command == "share_score") {
_delegate->botHandleMenuButton(MenuButton::ShareGame); _delegate->botHandleMenuButton(MenuButton::ShareGame);
} }
@ -745,6 +795,7 @@ postEvent: function(eventType, eventData) {
return false; return false;
} }
layoutButtons();
setupProgressGeometry(); setupProgressGeometry();
base::qt_signal_producer( base::qt_signal_producer(
@ -1096,12 +1147,12 @@ void Panel::requestClipboardText(const QJsonObject &args) {
} }
bool Panel::allowOpenLink() const { bool Panel::allowOpenLink() const {
const auto now = crl::now(); //const auto now = crl::now();
if (_mainButtonLastClick //if (_mainButtonLastClick
&& _mainButtonLastClick + kProcessClickTimeout >= now) { // && _mainButtonLastClick + kProcessClickTimeout >= now) {
_mainButtonLastClick = 0; // _mainButtonLastClick = 0;
return true; // return true;
} //}
return true; return true;
} }
@ -1109,12 +1160,12 @@ bool Panel::allowClipboardQuery() const {
if (!_allowClipboardRead) { if (!_allowClipboardRead) {
return false; return false;
} }
const auto now = crl::now(); //const auto now = crl::now();
if (_mainButtonLastClick //if (_mainButtonLastClick
&& _mainButtonLastClick + kProcessClickTimeout >= now) { // && _mainButtonLastClick + kProcessClickTimeout >= now) {
_mainButtonLastClick = 0; // _mainButtonLastClick = 0;
return true; // return true;
} //}
return true; return true;
} }
@ -1157,14 +1208,16 @@ void Panel::setupClosingBehaviour(const QJsonObject &args) {
_closeNeedConfirmation = args["need_confirmation"].toBool(); _closeNeedConfirmation = args["need_confirmation"].toBool();
} }
void Panel::processMainButtonMessage(const QJsonObject &args) { void Panel::processButtonMessage(
std::unique_ptr<Button> &button,
const QJsonObject &args) {
if (args.isEmpty()) { if (args.isEmpty()) {
_delegate->botClose(); _delegate->botClose();
return; return;
} }
const auto shown = [&] { const auto shown = [&] {
return _mainButton && !_mainButton->isHidden(); return button && !button->isHidden();
}; };
const auto wasShown = shown(); const auto wasShown = shown();
const auto guard = gsl::finally([&] { const auto guard = gsl::finally([&] {
@ -1175,42 +1228,38 @@ void Panel::processMainButtonMessage(const QJsonObject &args) {
} }
}); });
if (!_mainButton) { const auto text = args["text"].toString().trimmed();
if (args["is_visible"].toBool()) { const auto visible = args["is_visible"].toBool() && !text.isEmpty();
createMainButton(); if (!button) {
if (visible) {
createButton(button);
_bottomButtonsBg->show();
} else { } else {
return; return;
} }
} }
if (const auto bg = ParseColor(args["color"].toString())) { if (const auto bg = ParseColor(args["color"].toString())) {
_mainButton->updateBg(*bg); button->updateBg(*bg);
_bgLifetime.destroy();
} else { } else {
_mainButton->updateBg(st::windowBgActive->c); button->updateBg(&st::windowBgActive);
_bgLifetime = style::PaletteChanged(
) | rpl::start_with_next([=] {
_mainButton->updateBg(st::windowBgActive->c);
});
} }
if (const auto fg = ParseColor(args["text_color"].toString())) { if (const auto fg = ParseColor(args["text_color"].toString())) {
_mainButton->updateFg(*fg); button->updateFg(*fg);
_fgLifetime.destroy();
} else { } else {
_mainButton->updateFg(st::windowFgActive->c); button->updateFg(&st::windowFgActive);
_fgLifetime = style::PaletteChanged(
) | rpl::start_with_next([=] {
_mainButton->updateFg(st::windowFgActive->c);
});
} }
_mainButton->updateArgs({ button->updateArgs({
.isActive = args["is_active"].toBool(), .isActive = args["is_active"].toBool(),
.isVisible = args["is_visible"].toBool(), .isVisible = visible,
.isProgressVisible = args["is_progress_visible"].toBool(), .isProgressVisible = args["is_progress_visible"].toBool(),
.text = args["text"].toString(), .text = args["text"].toString(),
}); });
if (button.get() == _secondaryButton.get()) {
_secondaryPosition = ParsePosition(args["position"].toString());
}
} }
void Panel::processBackButtonMessage(const QJsonObject &args) { void Panel::processBackButtonMessage(const QJsonObject &args) {
@ -1225,11 +1274,12 @@ void Panel::processHeaderColor(const QJsonObject &args) {
if (const auto color = ParseColor(args["color"].toString())) { if (const auto color = ParseColor(args["color"].toString())) {
_widget->overrideTitleColor(color); _widget->overrideTitleColor(color);
_headerColorLifetime.destroy(); _headerColorLifetime.destroy();
} else if (args["color_key"].toString() == u"secondary_bg_color"_q) { } else if (const auto color = LookupNamedColor(
_widget->overrideTitleColor(st::boxDividerBg->c); args["color_key"].toString())) {
_widget->overrideTitleColor((*color)->c);
_headerColorLifetime = style::PaletteChanged( _headerColorLifetime = style::PaletteChanged(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
_widget->overrideTitleColor(st::boxDividerBg->c); _widget->overrideTitleColor((*color)->c);
}); });
} else { } else {
_widget->overrideTitleColor(std::nullopt); _widget->overrideTitleColor(std::nullopt);
@ -1237,37 +1287,146 @@ void Panel::processHeaderColor(const QJsonObject &args) {
} }
} }
void Panel::createMainButton() { void Panel::processBottomBarColor(const QJsonObject &args) {
_mainButton = std::make_unique<Button>( if (const auto color = ParseColor(args["color"].toString())) {
_widget.get(), _widget->overrideBottomBarColor(color);
st::botWebViewBottomButton); _bottomBarColor = color;
const auto button = _mainButton.get(); _bottomBarColorLifetime.destroy();
} else if (const auto color = LookupNamedColor(
button->setClickedCallback([=] { args["color_key"].toString())) {
if (!button->isDisabled()) { _widget->overrideBottomBarColor((*color)->c);
postEvent("main_button_pressed"); _bottomBarColor = (*color)->c;
_mainButtonLastClick = crl::now(); _headerColorLifetime = style::PaletteChanged(
} ) | rpl::start_with_next([=] {
}); _widget->overrideBottomBarColor((*color)->c);
button->hide(); _bottomBarColor = (*color)->c;
});
rpl::combine( } else {
_webviewParent->geometryValue() | rpl::map([=] { _widget->overrideBottomBarColor(std::nullopt);
return _widget->innerGeometry(); _bottomBarColor = std::nullopt;
}), _headerColorLifetime.destroy();
button->shownValue(), }
button->heightValue() if (const auto raw = _bottomButtonsBg.get()) {
) | rpl::start_with_next([=](QRect inner, bool shown, int height) { raw->update();
button->move(inner.x(), inner.y() + inner.height() - height); }
button->resizeToWidth(inner.width());
_webviewBottom->setVisible(!shown);
updateFooterHeight();
}, button->lifetime());
} }
void Panel::updateFooterHeight() { void Panel::createButton(std::unique_ptr<Button> &button) {
_footerHeight = (_mainButton && !_mainButton->isHidden()) if (!_bottomButtonsBg) {
? _mainButton->height() _bottomButtonsBg = std::make_unique<RpWidget>(_widget.get());
const auto raw = _bottomButtonsBg.get();
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(_bottomBarColor.value_or(st::windowBg->c));
p.drawRoundedRect(
raw->rect().marginsAdded({ 0, 2 * st::callRadius, 0, 0 }),
st::callRadius,
st::callRadius);
}, raw->lifetime());
}
button = std::make_unique<Button>(
_bottomButtonsBg.get(),
st::botWebViewBottomButton);
const auto raw = button.get();
raw->setClickedCallback([=] {
if (!raw->isDisabled()) {
if (raw == _mainButton.get()) {
postEvent("main_button_pressed");
} else if (raw == _secondaryButton.get()) {
postEvent("secondary_button_pressed");
}
}
});
raw->hide();
rpl::combine(
raw->shownValue(),
raw->heightValue()
) | rpl::start_with_next([=] {
layoutButtons();
}, raw->lifetime());
}
void Panel::layoutButtons() {
const auto inner = _widget->innerGeometry();
const auto shown = [](std::unique_ptr<Button> &button) {
return button && !button->isHidden();
};
const auto any = shown(_mainButton) || shown(_secondaryButton);
_webviewBottom->setVisible(!any);
if (any) {
_bottomButtonsBg->show();
const auto one = shown(_mainButton)
? _mainButton.get()
: _secondaryButton.get();
const auto both = shown(_mainButton) && shown(_secondaryButton);
const auto vertical = both
&& ((_secondaryPosition == RectPart::Top)
|| (_secondaryPosition == RectPart::Bottom));
const auto padding = st::botWebViewBottomPadding;
const auto height = padding.top()
+ (vertical
? (_mainButton->height()
+ st::botWebViewBottomSkip.y()
+ _secondaryButton->height())
: one->height())
+ padding.bottom();
_bottomButtonsBg->setGeometry(
inner.x(),
inner.y() + inner.height() - height,
inner.width(),
height);
auto left = padding.left();
auto bottom = height - padding.bottom();
auto available = inner.width() - padding.left() - padding.right();
if (!both) {
one->resizeToWidth(available);
one->move(left, bottom - one->height());
} else if (_secondaryPosition == RectPart::Top) {
_mainButton->resizeToWidth(available);
bottom -= _mainButton->height();
_mainButton->move(left, bottom);
bottom -= st::botWebViewBottomSkip.y();
_secondaryButton->resizeToWidth(available);
bottom -= _secondaryButton->height();
_secondaryButton->move(left, bottom);
} else if (_secondaryPosition == RectPart::Bottom) {
_secondaryButton->resizeToWidth(available);
bottom -= _secondaryButton->height();
_secondaryButton->move(left, bottom);
bottom -= st::botWebViewBottomSkip.y();
_mainButton->resizeToWidth(available);
bottom -= _mainButton->height();
_mainButton->move(left, bottom);
} else if (_secondaryPosition == RectPart::Left) {
available = (available - st::botWebViewBottomSkip.x()) / 2;
_secondaryButton->resizeToWidth(available);
bottom -= _secondaryButton->height();
_secondaryButton->move(left, bottom);
_mainButton->resizeToWidth(available);
_mainButton->move(
inner.width() - padding.right() - available,
bottom);
} else {
available = (available - st::botWebViewBottomSkip.x()) / 2;
_mainButton->resizeToWidth(available);
bottom -= _mainButton->height();
_mainButton->move(left, bottom);
_secondaryButton->resizeToWidth(available);
_secondaryButton->move(
inner.width() - padding.right() - available,
bottom);
}
} else if (_bottomButtonsBg) {
_bottomButtonsBg->hide();
}
_footerHeight = any
? _bottomButtonsBg->height()
: _webviewBottom->height(); : _webviewBottom->height();
} }

View file

@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h" #include "base/object_ptr.h"
#include "base/weak_ptr.h" #include "base/weak_ptr.h"
#include "base/flags.h" #include "base/flags.h"
#include "ui/rect_part.h"
#include "ui/round_rect.h"
#include "webview/webview_common.h" #include "webview/webview_common.h"
class QJsonObject; class QJsonObject;
@ -32,13 +34,6 @@ namespace Ui::BotWebView {
[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info); [[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);
struct MainButtonArgs {
bool isActive = false;
bool isVisible = false;
bool isProgressVisible = false;
QString text;
};
enum class MenuButton { enum class MenuButton {
None = 0x00, None = 0x00,
OpenBot = 0x01, OpenBot = 0x01,
@ -126,10 +121,13 @@ private:
void setTitle(rpl::producer<QString> title); void setTitle(rpl::producer<QString> title);
void sendDataMessage(const QJsonObject &args); void sendDataMessage(const QJsonObject &args);
void switchInlineQueryMessage(const QJsonObject &args); void switchInlineQueryMessage(const QJsonObject &args);
void processMainButtonMessage(const QJsonObject &args); void processButtonMessage(
std::unique_ptr<Button> &button,
const QJsonObject &args);
void processBackButtonMessage(const QJsonObject &args); void processBackButtonMessage(const QJsonObject &args);
void processSettingsButtonMessage(const QJsonObject &args); void processSettingsButtonMessage(const QJsonObject &args);
void processHeaderColor(const QJsonObject &args); void processHeaderColor(const QJsonObject &args);
void processBottomBarColor(const QJsonObject &args);
void openTgLink(const QJsonObject &args); void openTgLink(const QJsonObject &args);
void openExternalLink(const QJsonObject &args); void openExternalLink(const QJsonObject &args);
void openInvoice(const QJsonObject &args); void openInvoice(const QJsonObject &args);
@ -144,7 +142,7 @@ private:
void replyCustomMethod(QJsonValue requestId, QJsonObject response); void replyCustomMethod(QJsonValue requestId, QJsonObject response);
void requestClipboardText(const QJsonObject &args); void requestClipboardText(const QJsonObject &args);
void setupClosingBehaviour(const QJsonObject &args); void setupClosingBehaviour(const QJsonObject &args);
void createMainButton(); void createButton(std::unique_ptr<Button> &button);
void scheduleCloseWithConfirmation(); void scheduleCloseWithConfirmation();
void closeWithConfirmation(); void closeWithConfirmation();
void sendViewport(); void sendViewport();
@ -158,7 +156,7 @@ private:
[[nodiscard]] bool progressWithBackground() const; [[nodiscard]] bool progressWithBackground() const;
[[nodiscard]] QRect progressRect() const; [[nodiscard]] QRect progressRect() const;
void setupProgressGeometry(); void setupProgressGeometry();
void updateFooterHeight(); void layoutButtons();
Webview::StorageId _storageId; Webview::StorageId _storageId;
const not_null<Delegate*> _delegate; const not_null<Delegate*> _delegate;
@ -170,14 +168,16 @@ private:
std::unique_ptr<RpWidget> _webviewBottom; std::unique_ptr<RpWidget> _webviewBottom;
rpl::variable<QString> _bottomText; rpl::variable<QString> _bottomText;
QPointer<RpWidget> _webviewParent; QPointer<RpWidget> _webviewParent;
std::unique_ptr<RpWidget> _bottomButtonsBg;
std::unique_ptr<Button> _mainButton; std::unique_ptr<Button> _mainButton;
mutable crl::time _mainButtonLastClick = 0; std::unique_ptr<Button> _secondaryButton;
RectPart _secondaryPosition = RectPart::Left;
rpl::variable<int> _footerHeight = 0; rpl::variable<int> _footerHeight = 0;
std::unique_ptr<Progress> _progress; std::unique_ptr<Progress> _progress;
rpl::event_stream<> _themeUpdateForced; rpl::event_stream<> _themeUpdateForced;
std::optional<QColor> _bottomBarColor;
rpl::lifetime _headerColorLifetime; rpl::lifetime _headerColorLifetime;
rpl::lifetime _fgLifetime; rpl::lifetime _bottomBarColorLifetime;
rpl::lifetime _bgLifetime;
bool _webviewProgress = false; bool _webviewProgress = false;
bool _themeUpdateScheduled = false; bool _themeUpdateScheduled = false;
bool _hiddenForPayment = false; bool _hiddenForPayment = false;

View file

@ -1503,6 +1503,7 @@ bool ReadPaletteValues(const QByteArray &content, Fn<bool(QLatin1String name, QL
{ "section_header_text_color", st::windowActiveTextFg }, { "section_header_text_color", st::windowActiveTextFg },
{ "subtitle_text_color", st::windowSubTextFg }, { "subtitle_text_color", st::windowSubTextFg },
{ "destructive_text_color", st::attentionButtonFg }, { "destructive_text_color", st::attentionButtonFg },
{ "bottom_bar_bg_color", st::windowBg },
}; };
auto object = QJsonObject(); auto object = QJsonObject();
const auto wrap = [](QColor color) { const auto wrap = [](QColor color) {

@ -1 +1 @@
Subproject commit 9ca74272a0ec33bac83c5b61a73d523902eb96cd Subproject commit f90a42009d8dd28c818bb3c6916f4303fb9bcf0c