init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
/*
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 "ui/boxes/auto_delete_settings.h"
#include "ui/widgets/checkbox.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace Ui {
namespace {
object_ptr<Ui::RpWidget> CreateSliderForTTL(
not_null<QWidget*> parent,
std::vector<QString> labels,
int dashedAfterIndex,
int selected,
Fn<void(int)> callback) {
Expects(labels.size() > 1);
Expects(selected >= 0 && selected < labels.size());
Expects(dashedAfterIndex >= 0 && dashedAfterIndex < labels.size());
struct State {
std::vector<int> points;
std::vector<QString> labels;
int selected = 0;
};
static const auto st = &st::defaultSliderForTTL;
const auto height = st->font->height + st->skip + st->chosenSize;
const auto count = int(labels.size());
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);
const auto raw = result.data();
const auto slider = Ui::CreateChild<Ui::FixedHeightWidget>(
raw,
st->chosenSize);
slider->setCursor(style::cur_pointer);
slider->move(0, height - slider->height());
auto &lifetime = raw->lifetime();
const auto state = lifetime.make_state<State>(State{
.labels = std::move(labels),
.selected = selected
});
state->points.resize(count, 0);
raw->widthValue(
) | rpl::on_next([=](int width) {
for (auto i = 0; i != count; ++i) {
state->points[i] = (width * i) / (count - 1);
}
slider->resize(width, slider->height());
}, lifetime);
raw->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(raw);
p.setFont(st->font);
for (auto i = 0; i != count; ++i) {
// Label
p.setPen(st->textFg);
const auto &text = state->labels[i];
const auto textWidth = st->font->width(text);
const auto shift = (i == count - 1)
? textWidth
: (i > 0)
? (textWidth / 2)
: 0;
const auto x = state->points[i] - shift;
const auto y = st->font->ascent;
p.drawText(x, y, text);
}
}, lifetime);
slider->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(slider);
auto hq = PainterHighQualityEnabler(p);
p.setFont(st->font);
for (auto i = 0; i != count; ++i) {
const auto middle = (st->chosenSize / 2.);
// Point
const auto size = (i == state->selected)
? st->chosenSize
: st->pointSize;
const auto pointfg = (i <= state->selected)
? st->activeFg
: st->inactiveFg;
const auto shift = (i == count - 1)
? float64(size)
: (i > 0)
? (size / 2.)
: 0.;
const auto pointx = state->points[i] - shift;
const auto pointy = middle - (size / 2.);
p.setPen(Qt::NoPen);
p.setBrush(pointfg);
p.drawEllipse(QRectF{ pointx, pointy, size * 1., size * 1. });
// Line
if (i + 1 == count) {
break;
}
const auto nextSize = (i + 1 == state->selected)
? st->chosenSize
: st->pointSize;
const auto nextShift = (i + 1 == count - 1)
? float64(nextSize)
: (nextSize / 2.);
const auto &linefg = (i + 1 <= state->selected)
? st->activeFg
: st->inactiveFg;
const auto from = pointx + size + st->stroke * 1.5;
const auto till = state->points[i + 1] - nextShift - st->stroke * 1.5;
auto pen = linefg->p;
pen.setWidthF(st->stroke);
if (i >= dashedAfterIndex) {
// Try to fill the line with exact number of dash segments.
// UPD Doesn't work so well because it changes when clicking.
//const auto length = till - from;
//const auto offSegmentsCount = int(base::SafeRound(
// (length - st->dashOn) / (st->dashOn + st->dashOff)));
//const auto onSegmentsCount = offSegmentsCount + 1;
//const auto idealLength = offSegmentsCount * st->dashOff
// + onSegmentsCount * st->dashOn;
//const auto multiplier = length / float64(idealLength);
const auto multiplier = 1.;
auto dashPattern = QVector<qreal>{
st->dashOn * multiplier / st->stroke,
st->dashOff * multiplier / st->stroke
};
pen.setDashPattern(dashPattern);
}
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawLine(QPointF(from, middle), QPointF(till, middle));
}
}, lifetime);
slider->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::MouseButtonPress)
&& (static_cast<QMouseEvent*>(e.get())->button()
== Qt::LeftButton)
&& (state->points[1] > 0);
}) | rpl::map([=](not_null<QEvent*> e) {
return rpl::single(
static_cast<QMouseEvent*>(e.get())->pos()
) | rpl::then(slider->events(
) | rpl::take_while([=](not_null<QEvent*> e) {
return (e->type() != QEvent::MouseButtonRelease)
|| (static_cast<QMouseEvent*>(e.get())->button()
!= Qt::LeftButton);
}) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::MouseMove);
}) | rpl::map([=](not_null<QEvent*> e) {
return static_cast<QMouseEvent*>(e.get())->pos();
}));
}) | rpl::flatten_latest(
) | rpl::on_next([=](QPoint position) {
state->selected = std::clamp(
(position.x() + (state->points[1] / 2)) / state->points[1],
0,
count - 1);
slider->update();
callback(state->selected);
}, lifetime);
return result;
}
} // namespace
void AutoDeleteSettingsBox(
not_null<Ui::GenericBox*> box,
TimeId ttlPeriod,
rpl::producer<QString> about,
Fn<void(TimeId)> callback) {
box->setTitle(tr::lng_manage_messages_ttl_title());
struct State {
TimeId period = 0;
};
const auto state = box->lifetime().make_state<State>(State{
.period = ttlPeriod,
});
const auto options = std::vector<QString>{
tr::lng_manage_messages_ttl_disable(tr::now),
//u"5 seconds"_q, AssertIsDebug()
tr::lng_manage_messages_ttl_after1(tr::now),
tr::lng_manage_messages_ttl_after2(tr::now),
tr::lng_manage_messages_ttl_after3(tr::now),
};
const auto periodToIndex = [&](TimeId period) {
return !period
? 0
//: (period == 5) AssertIsDebug()
//? 1 AssertIsDebug()
: (period < 2 * 86400)
? 1
: (period < 8 * 86400)
? 2
: 3;
};
const auto indexToPeriod = [&](int index) {
return !index
? 0
//: (index == 1) AssertIsDebug()
//? 5 AssertIsDebug()
: (index == 1)
? 86400
: (index == 2)
? 7 * 86400
: 31 * 86400;
};
const auto sliderCallback = [=](int index) {
state->period = indexToPeriod(index);
};
box->addRow(
CreateSliderForTTL(
box,
options | ranges::to_vector,
options.size() - 1,
periodToIndex(ttlPeriod),
sliderCallback),
{
st::boxRowPadding.left(),
0,
st::boxRowPadding.right(),
st::boxMediumSkip });
box->addRow(
object_ptr<Ui::DividerLabel>(
box,
object_ptr<Ui::FlatLabel>(
box,
std::move(about),
st::boxDividerLabel),
st::ttlDividerLabelPadding),
style::margins());
box->addButton(tr::lng_settings_save(), [=] {
const auto period = state->period;
box->closeBox();
callback(period);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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 "ui/layers/generic_box.h"
namespace Ui {
void AutoDeleteSettingsBox(
not_null<Ui::GenericBox*> box,
TimeId ttlPeriod,
rpl::producer<QString> about,
Fn<void(TimeId)> callback);
} // namespace Ui

View File

@@ -0,0 +1,986 @@
/*
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 "ui/boxes/boost_box.h"
#include "info/profile/info_profile_icon.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/fireworks_animation.h"
#include "ui/effects/premium_bubble.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_giveaway.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
[[nodiscard]] BoostCounters AdjustByReached(BoostCounters data) {
const auto exact = (data.boosts == data.thisLevelBoosts);
const auto reached = !data.nextLevelBoosts || (exact && data.mine > 0);
if (reached) {
--data.level;
data.boosts = data.nextLevelBoosts = std::max({
data.boosts,
data.thisLevelBoosts,
1
});
data.thisLevelBoosts = 0;
} else {
data.boosts = std::max(data.thisLevelBoosts, data.boosts);
data.nextLevelBoosts = std::max(
data.nextLevelBoosts,
data.boosts + 1);
}
return data;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> title,
rpl::producer<QString> repeated,
bool centered = true) {
auto result = object_ptr<Ui::RpWidget>(parent);
struct State {
not_null<Ui::FlatLabel*> title;
not_null<Ui::FlatLabel*> repeated;
};
const auto notEmpty = [](const QString &text) {
return !text.isEmpty();
};
const auto state = parent->lifetime().make_state<State>(State{
.title = Ui::CreateChild<Ui::FlatLabel>(
result.data(),
rpl::duplicate(title),
st::boostTitle),
.repeated = Ui::CreateChild<Ui::FlatLabel>(
result.data(),
rpl::duplicate(repeated) | rpl::filter(notEmpty),
st::boostTitleBadge),
});
state->title->show();
state->repeated->showOn(std::move(repeated) | rpl::map(notEmpty));
result->resize(result->width(), st::boostTitle.style.font->height);
rpl::combine(
result->widthValue(),
rpl::duplicate(title),
state->repeated->shownValue(),
state->repeated->widthValue()
) | rpl::on_next([=](int outer, auto&&, bool shown, int badge) {
const auto repeated = shown ? badge : 0;
const auto skip = st::boostTitleBadgeSkip;
const auto available = outer - repeated - skip;
const auto use = std::min(state->title->textMaxWidth(), available);
state->title->resizeToWidth(use);
const auto left = centered
? (outer - use - skip - repeated) / 2
: 0;
state->title->moveToLeft(left, 0);
const auto mleft = st::boostTitleBadge.margin.left();
const auto mtop = st::boostTitleBadge.margin.top();
state->repeated->moveToLeft(left + use + skip + mleft, mtop);
}, result->lifetime());
const auto badge = state->repeated;
badge->paintRequest() | rpl::on_next([=] {
auto p = QPainter(badge);
auto hq = PainterHighQualityEnabler(p);
const auto radius = std::min(badge->width(), badge->height()) / 2;
p.setPen(Qt::NoPen);
p.setBrush(st::premiumButtonBg2);
p.drawRoundedRect(badge->rect(), radius, radius);
}, badge->lifetime());
return result;
}
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text) {
return MakeBoostFeaturesBadge(parent, std::move(text), [](QRect rect) {
auto gradient = QLinearGradient(
rect.topLeft(),
rect.topRight());
gradient.setStops(Ui::Premium::GiftGradientStops());
return QBrush(gradient);
});
}
void AddFeaturesList(
not_null<Ui::VerticalLayout*> container,
const Ui::BoostFeatures &features,
int startFromLevel,
bool group) {
const auto add = [&](
rpl::producer<TextWithEntities> text,
const style::icon &st) {
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
std::move(text),
st::boostFeatureLabel),
st::boostFeaturePadding);
object_ptr<Info::Profile::FloatingIcon>(
label,
st,
st::boostFeatureIconPosition);
};
const auto lowMax = std::max({
features.linkLogoLevel,
features.profileIconLevel,
features.autotranslateLevel,
features.transcribeLevel,
features.emojiPackLevel,
features.emojiStatusLevel,
features.wallpaperLevel,
features.customWallpaperLevel,
(features.nameColorsByLevel.empty()
? 0
: features.nameColorsByLevel.back().first),
(features.linkStylesByLevel.empty()
? 0
: features.linkStylesByLevel.back().first),
(features.profileColorsByLevel.empty()
? 0
: features.profileColorsByLevel.back().first),
});
const auto highMax = std::max(lowMax, features.sponsoredLevel);
auto nameColors = 0;
auto linkStyles = 0;
auto profileColors = 0;
for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) {
if ((i > lowMax) && (i < highMax)) {
continue;
}
const auto unlocks = (i == startFromLevel);
{
const auto badge = container->add(
MakeFeaturesBadge(
container,
(unlocks
? tr::lng_boost_level_unlocks
: tr::lng_boost_level)(
lt_count,
rpl::single(float64(i)))),
st::boostLevelBadgePadding,
style::al_top);
const auto padding = st::boxRowPadding;
const auto line = Ui::CreateChild<Ui::RpWidget>(container);
badge->geometryValue() | rpl::on_next([=](const QRect &r) {
line->setGeometry(
padding.left(),
r.y(),
container->width() - rect::m::sum::h(padding),
r.height());
}, line->lifetime());
const auto shift = st::lineWidth * 10;
line->paintRequest() | rpl::on_next([=] {
auto p = QPainter(line);
p.setPen(st::windowSubTextFg);
const auto y = line->height() / 2;
const auto left = badge->x() - shift - padding.left();
const auto right = left + badge->width() + shift * 2;
if (left > 0) {
p.drawLine(0, y, left, y);
}
if (right < line->width()) {
p.drawLine(right, y, line->width(), y);
}
}, line->lifetime());
}
if (i >= features.sponsoredLevel) {
add(
tr::lng_channel_earn_off(tr::rich),
st::boostFeatureOffSponsored);
}
if (i >= features.customWallpaperLevel) {
add(
(group
? tr::lng_feature_custom_background_group
: tr::lng_feature_custom_background_channel)(tr::rich),
st::boostFeatureCustomBackground);
}
if (i >= features.wallpaperLevel) {
add(
(group
? tr::lng_feature_backgrounds_group
: tr::lng_feature_backgrounds_channel)(
lt_count,
rpl::single(float64(features.wallpapersCount)),
tr::rich),
st::boostFeatureBackground);
}
if (i >= features.emojiStatusLevel) {
add(
tr::lng_feature_emoji_status(tr::rich),
st::boostFeatureEmojiStatus);
}
if (const auto j = features.profileColorsByLevel.find(i)
; j != end(features.profileColorsByLevel)) {
profileColors += j->second;
}
if (i >= features.profileIconLevel) {
add(
(group
? tr::lng_feature_profile_icon_group
: tr::lng_feature_profile_icon_channel)(tr::rich),
st::boostFeatureProfileIcon);
}
if (profileColors > 0) {
add((group
? tr::lng_feature_profile_color_group
: tr::lng_feature_profile_color_channel)(
lt_count,
rpl::single(float64(profileColors)),
tr::rich
), st::boostFeatureProfileColor);
}
if (!group) {
if (const auto j = features.linkStylesByLevel.find(i)
; j != end(features.linkStylesByLevel)) {
linkStyles += j->second;
}
if (i >= features.linkLogoLevel) {
add(
tr::lng_feature_link_emoji(tr::rich),
st::boostFeatureCustomLink);
}
if (linkStyles > 0) {
add(tr::lng_feature_link_style_channel(
lt_count,
rpl::single(float64(linkStyles)),
tr::rich
), st::boostFeatureLink);
}
if (const auto j = features.nameColorsByLevel.find(i)
; j != end(features.nameColorsByLevel)) {
nameColors += j->second;
}
if (nameColors > 0) {
add(tr::lng_feature_name_color_channel(
lt_count,
rpl::single(float64(nameColors)),
tr::rich
), st::boostFeatureName);
}
add(tr::lng_feature_reactions(
lt_count,
rpl::single(float64(i)),
tr::rich
), st::boostFeatureCustomReactions);
}
add(
tr::lng_feature_stories(lt_count, rpl::single(1. * i), tr::rich),
st::boostFeatureStories);
if (!group && i >= features.autotranslateLevel) {
add(
tr::lng_feature_autotranslate(tr::rich),
st::boostFeatureAutoTranslate);
}
if (group && i >= features.transcribeLevel) {
add(
tr::lng_feature_transcribe(tr::rich),
st::boostFeatureTranscribe);
}
if (group && i >= features.emojiPackLevel) {
add(
tr::lng_feature_custom_emoji_pack(tr::rich),
st::boostFeatureCustomEmoji);
}
}
}
} // namespace
void StartFireworks(not_null<QWidget*> parent) {
const auto result = Ui::CreateChild<RpWidget>(parent.get());
result->setAttribute(Qt::WA_TransparentForMouseEvents);
result->setGeometry(parent->rect());
result->show();
auto &lifetime = result->lifetime();
const auto animation = lifetime.make_state<FireworksAnimation>([=] {
result->update();
});
result->paintRequest() | rpl::on_next([=] {
auto p = QPainter(result);
if (!animation->paint(p, result->rect())) {
crl::on_main(result, [=] { delete result; });
}
}, lifetime);
}
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(BoostCounters)>)> boost) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
//AssertIsDebug();
//data.boost = {
// .level = 2,
// .boosts = 3,
// .thisLevelBoosts = 2,
// .nextLevelBoosts = 5,
// .mine = 2,
//};
struct State {
rpl::variable<BoostCounters> data;
rpl::variable<bool> full;
bool submitted = false;
};
const auto state = box->lifetime().make_state<State>();
state->data = std::move(data.boost);
FillBoostLimit(
BoxShowFinishes(box),
box->verticalLayout(),
state->data.value(),
st::boxRowPadding);
box->setMaxHeight(st::boostBoxMaxHeight);
const auto close = box->addTopButton(
st::boxTitleClose,
[=] { box->closeBox(); });
const auto name = data.name;
auto title = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (counters.mine > 0)
? tr::lng_boost_channel_you_title(
lt_channel,
rpl::single(name))
: !counters.nextLevelBoosts
? tr::lng_boost_channel_title_max()
: counters.level
? (data.group
? tr::lng_boost_channel_title_more_group()
: tr::lng_boost_channel_title_more())
: (data.group
? tr::lng_boost_channel_title_first_group()
: tr::lng_boost_channel_title_first());
}) | rpl::flatten_latest();
auto repeated = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (counters.mine > 1) ? u"x%1"_q.arg(counters.mine) : u""_q;
});
const auto wasMine = state->data.current().mine;
const auto wasLifting = data.lifting;
auto text = state->data.value(
) | rpl::map([=](BoostCounters counters) {
const auto lifting = wasLifting
? (wasLifting
- std::clamp(counters.mine - wasMine, 0, wasLifting - 1))
: 0;
const auto bold = tr::bold(name);
const auto now = counters.boosts;
const auto full = !counters.nextLevelBoosts;
const auto left = (counters.nextLevelBoosts > now)
? (counters.nextLevelBoosts - now)
: 0;
auto post = tr::lng_boost_channel_post_stories(
lt_count,
rpl::single(float64(counters.level + (left ? 1 : 0))),
tr::rich);
return (lifting > 1)
? tr::lng_boost_group_lift_restrictions_many(
lt_count,
rpl::single(float64(lifting)),
tr::rich)
: lifting
? tr::lng_boost_group_lift_restrictions(tr::rich)
: (counters.mine || full)
? (left
? tr::lng_boost_channel_needs_unlock(
lt_count,
rpl::single(float64(left)),
lt_channel,
rpl::single(bold),
tr::rich)
: (!counters.level
? (data.group
? tr::lng_boost_channel_reached_first_group
: tr::lng_boost_channel_reached_first)(
tr::rich)
: (data.group
? tr::lng_boost_channel_reached_more_group
: tr::lng_boost_channel_reached_more)(
lt_count,
rpl::single(float64(counters.level)),
lt_post,
std::move(post),
tr::rich)))
: tr::lng_boost_channel_needs_unlock(
lt_count,
rpl::single(float64(left)),
lt_channel,
rpl::single(bold),
tr::rich);
}) | rpl::flatten_latest();
if (wasLifting) {
state->data.value(
) | rpl::on_next([=](BoostCounters counters) {
if (counters.mine - wasMine >= wasLifting) {
box->closeBox();
}
}, box->lifetime());
}
auto faded = object_ptr<Ui::FadeWrap<>>(
close->parentWidget(),
MakeTitle(
box,
(data.group
? tr::lng_boost_group_button
: tr::lng_boost_channel_button)(),
rpl::duplicate(repeated),
false));
const auto titleInner = faded.data();
titleInner->move(st::boxTitlePosition);
titleInner->resizeToWidth(st::boxWideWidth
- st::boxTitleClose.width
- st::boxTitlePosition.x());
titleInner->hide(anim::type::instant);
crl::on_main(titleInner, [=] {
titleInner->raise();
titleInner->toggleOn(rpl::single(
rpl::empty
) | rpl::then(
box->scrolls()
) | rpl::map([=] {
return box->scrollTop() > 0;
}));
});
box->addRow(
MakeTitle(box, std::move(title), std::move(repeated)),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
const auto current = state->data.current();
box->setTitle(rpl::single(QString()));
AddFeaturesList(
box->verticalLayout(),
data.features,
current.level + (current.nextLevelBoosts ? 1 : 0),
data.group);
const auto allowMulti = data.allowMulti;
auto submit = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (!counters.nextLevelBoosts || (counters.mine && !allowMulti))
? tr::lng_box_ok()
: (counters.mine > 0)
? tr::lng_boost_again_button()
: data.group
? tr::lng_boost_group_button()
: tr::lng_boost_channel_button();
}) | rpl::flatten_latest();
box->addButton(rpl::duplicate(submit), [=] {
if (state->submitted) {
return;
} else if (state->data.current().nextLevelBoosts > 0
&& (allowMulti || !state->data.current().mine)) {
state->submitted = true;
const auto was = state->data.current().mine;
//AssertIsDebug();
//state->submitted = false;
//if (state->data.current().level == 5
// && state->data.current().boosts == 11) {
// state->data = BoostCounters{
// .level = 5,
// .boosts = 14,
// .thisLevelBoosts = 9,
// .nextLevelBoosts = 15,
// .mine = 14,
// };
//} else if (state->data.current().level == 5) {
// state->data = BoostCounters{
// .level = 7,
// .boosts = 16,
// .thisLevelBoosts = 15,
// .nextLevelBoosts = 19,
// .mine = 16,
// };
//} else if (state->data.current().level == 4) {
// state->data = BoostCounters{
// .level = 5,
// .boosts = 11,
// .thisLevelBoosts = 9,
// .nextLevelBoosts = 15,
// .mine = 9,
// };
//} else if (state->data.current().level == 3) {
// state->data = BoostCounters{
// .level = 4,
// .boosts = 7,
// .thisLevelBoosts = 7,
// .nextLevelBoosts = 9,
// .mine = 5,
// };
//} else {
// state->data = BoostCounters{
// .level = 3,
// .boosts = 5,
// .thisLevelBoosts = 5,
// .nextLevelBoosts = 7,
// .mine = 3,
// };
//}
//return;
boost(crl::guard(box, [=](BoostCounters result) {
state->submitted = false;
if (result.thisLevelBoosts || result.nextLevelBoosts) {
if (result.mine > was) {
StartFireworks(box->parentWidget());
}
state->data = result;
}
}));
} else {
box->closeBox();
}
});
}
object_ptr<Ui::RpWidget> MakeLinkLabel(
not_null<QWidget*> parent,
rpl::producer<QString> text,
rpl::producer<QString> link,
std::shared_ptr<Ui::Show> show,
object_ptr<Ui::RpWidget> right) {
auto result = object_ptr<Ui::AbstractButton>(parent);
const auto raw = result.data();
const auto rawRight = right.release();
if (rawRight) {
rawRight->setParent(raw);
rawRight->show();
}
struct State {
State(
not_null<QWidget*> parent,
rpl::producer<QString> value,
rpl::producer<QString> link)
: text(std::move(value))
, link(std::move(link))
, label(parent, text.value(), st::giveawayGiftCodeLink)
, bg(st::roundRadiusLarge, st::windowBgOver) {
}
rpl::variable<QString> text;
rpl::variable<QString> link;
Ui::FlatLabel label;
Ui::RoundRect bg;
};
const auto state = raw->lifetime().make_state<State>(
raw,
rpl::duplicate(text),
std::move(link));
state->label.setSelectable(true);
rpl::combine(
raw->widthValue(),
std::move(text)
) | rpl::on_next([=](int outer, const auto&) {
const auto textWidth = state->label.textMaxWidth();
const auto skipLeft = st::giveawayGiftCodeLink.margin.left();
const auto skipRight = rawRight
? rawRight->width()
: st::giveawayGiftCodeLink.margin.right();
const auto available = outer - skipRight - skipLeft;
const auto use = std::min(textWidth, available);
state->label.resizeToWidth(use);
const auto forCenter = (outer - use) / 2;
const auto x = (forCenter < skipLeft)
? skipLeft
: (forCenter > outer - skipRight - use)
? (outer - skipRight - use)
: forCenter;
state->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top());
}, raw->lifetime());
raw->paintRequest() | rpl::on_next([=] {
auto p = QPainter(raw);
state->bg.paint(p, raw->rect());
}, raw->lifetime());
state->label.setAttribute(Qt::WA_TransparentForMouseEvents);
raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight);
if (rawRight) {
raw->widthValue() | rpl::on_next([=](int width) {
rawRight->move(width - rawRight->width(), 0);
}, raw->lifetime());
}
raw->setClickedCallback([=] {
QGuiApplication::clipboard()->setText(state->link.current());
show->showToast(tr::lng_username_copied(tr::now));
});
return result;
}
void BoostBoxAlready(not_null<GenericBox*> box, bool group) {
ConfirmBox(box, {
.text = (group
? tr::lng_boost_error_already_text_group
: tr::lng_boost_error_already_text)(tr::rich),
.title = tr::lng_boost_error_already_title(),
.inform = true,
});
}
void GiftForBoostsBox(
not_null<GenericBox*> box,
QString channel,
int receive,
bool again) {
ConfirmBox(box, {
.text = (again
? tr::lng_boost_need_more_again
: tr::lng_boost_need_more_text)(
lt_count,
rpl::single(receive) | tr::to_count(),
lt_channel,
rpl::single(TextWithEntities{ channel }),
tr::rich),
.title = tr::lng_boost_need_more(),
.inform = true,
});
}
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group) {
InformBox(box, {
.text = (group
? tr::lng_boost_error_gifted_text_group
: tr::lng_boost_error_gifted_text)(tr::rich),
.title = tr::lng_boost_error_gifted_title(),
});
}
void PremiumForBoostsBox(
not_null<GenericBox*> box,
bool group,
Fn<void()> buyPremium) {
ConfirmBox(box, {
.text = (group
? tr::lng_boost_error_premium_text_group
: tr::lng_boost_error_premium_text)(tr::rich),
.confirmed = buyPremium,
.confirmText = tr::lng_boost_error_premium_yes(),
.title = tr::lng_boost_error_premium_title(),
});
}
void AskBoostBox(
not_null<GenericBox*> box,
AskBoostBoxData data,
Fn<void()> openStatistics,
Fn<void()> startGiveaway) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
box->setNoContentMargin(true);
box->addSkip(st::boxRowPadding.left());
FillBoostLimit(
BoxShowFinishes(box),
box->verticalLayout(),
rpl::single(data.boost),
st::boxRowPadding);
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
auto title = v::match(data.reason.data, [](AskBoostChannelColor) {
return tr::lng_boost_channel_title_color();
}, [](AskBoostAutotranslate) {
return tr::lng_boost_channel_title_autotranslate();
}, [](AskBoostWallpaper) {
return tr::lng_boost_channel_title_wallpaper();
}, [](AskBoostEmojiStatus) {
return tr::lng_boost_channel_title_status();
}, [](AskBoostEmojiPack) {
return tr::lng_boost_group_title_emoji();
}, [](AskBoostCustomReactions) {
return tr::lng_boost_channel_title_reactions();
}, [](AskBoostCpm) {
return tr::lng_boost_channel_title_cpm();
}, [](AskBoostWearCollectible) {
return tr::lng_boost_channel_title_wear();
});
auto isGroup = false;
auto reasonText = v::match(data.reason.data, [&](
AskBoostChannelColor data) {
return tr::lng_boost_channel_needs_level_color(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostAutotranslate data) {
return tr::lng_boost_channel_needs_level_autotranslate(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostWallpaper data) {
isGroup = data.group;
return (data.group
? tr::lng_boost_group_needs_level_wallpaper
: tr::lng_boost_channel_needs_level_wallpaper)(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostEmojiStatus data) {
isGroup = data.group;
return (data.group
? tr::lng_boost_group_needs_level_status
: tr::lng_boost_channel_needs_level_status)(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostEmojiPack data) {
isGroup = true;
return tr::lng_boost_group_needs_level_emoji(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostCustomReactions data) {
return tr::lng_boost_channel_needs_level_reactions(
lt_count,
rpl::single(float64(data.count)),
lt_same_count,
rpl::single(TextWithEntities{ QString::number(data.count) }),
tr::rich);
}, [&](AskBoostCpm data) {
return tr::lng_boost_channel_needs_level_cpm(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostWearCollectible data) {
return tr::lng_boost_channel_needs_level_wear(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
});
auto text = rpl::combine(
std::move(reasonText),
(isGroup ? tr::lng_boost_group_ask : tr::lng_boost_channel_ask)(
tr::rich)
) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {
return text.append(u"\n\n"_q).append(std::move(ask));
});
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(title),
st::boostCenteredTitle),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0),
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),
style::al_top);
auto stats = object_ptr<Ui::IconButton>(box, st::boostLinkStatsButton);
stats->setClickedCallback(openStatistics);
box->addRow(MakeLinkLabel(
box,
rpl::single(data.link),
rpl::single(data.link),
box->uiShow(),
std::move(stats)));
AddFeaturesList(
box->verticalLayout(),
data.features,
data.boost.level + (data.boost.nextLevelBoosts ? 1 : 0),
data.group);
auto submit = tr::lng_boost_channel_ask_button();
box->addButton(rpl::duplicate(submit), [=] {
QGuiApplication::clipboard()->setText(data.link);
box->uiShow()->showToast(tr::lng_username_copied(tr::now));
});
}
void FillBoostLimit(
rpl::producer<> showFinished,
not_null<VerticalLayout*> container,
rpl::producer<BoostCounters> data,
style::margins limitLinePadding) {
const auto addSkip = [&](int skip) {
container->add(object_ptr<Ui::FixedHeightWidget>(container, skip));
};
const auto ratio = [=](BoostCounters counters) {
const auto min = counters.thisLevelBoosts;
const auto max = counters.nextLevelBoosts;
Assert(counters.boosts >= min && counters.boosts <= max);
const auto count = (max - min);
const auto index = (counters.boosts - min);
if (!index) {
return 0.;
} else if (index == count) {
return 1.;
} else if (count == 2) {
return 0.5;
}
const auto available = st::boxWideWidth
- st::boxPadding.left()
- st::boxPadding.right();
const auto average = available / float64(count);
const auto levelWidth = [&](int add) {
return st::normalFont->width(
tr::lng_boost_level(
tr::now,
lt_count,
counters.level + add));
};
const auto paddings = 2 * st::premiumLineTextSkip;
const auto labelLeftWidth = paddings + levelWidth(0);
const auto labelRightWidth = paddings + levelWidth(1);
const auto first = std::max(average, labelLeftWidth * 1.);
const auto last = std::max(average, labelRightWidth * 1.);
const auto other = (available - first - last) / (count - 2);
return (first + (index - 1) * other) / available;
};
auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);
auto bubbleRowState = rpl::duplicate(
adjustedData
) | rpl::combine_previous(
BoostCounters()
) | rpl::map([=](BoostCounters previous, BoostCounters counters) {
return Premium::BubbleRowState{
.counter = counters.boosts,
.ratio = ratio(counters),
.animateFromZero = (counters.level != previous.level),
.dynamic = true,
};
});
Premium::AddBubbleRow(
container,
st::boostBubble,
std::move(showFinished),
rpl::duplicate(bubbleRowState),
Premium::BubbleType::Premium,
nullptr,
&st::premiumIconBoost,
limitLinePadding);
addSkip(st::premiumLineTextSkip);
const auto level = [](int level) {
return tr::lng_boost_level(tr::now, lt_count, level);
};
auto limitState = std::move(
bubbleRowState
) | rpl::map([](const Premium::BubbleRowState &state) {
return Premium::LimitRowState{
.ratio = state.ratio,
.animateFromZero = state.animateFromZero,
.dynamic = state.dynamic
};
});
auto left = rpl::duplicate(
adjustedData
) | rpl::map([=](BoostCounters counters) {
return level(counters.level);
});
auto right = rpl::duplicate(
adjustedData
) | rpl::map([=](BoostCounters counters) {
return level(counters.level + 1);
});
Premium::AddLimitRow(
container,
st::boostLimits,
Premium::LimitRowLabels{
.leftLabel = std::move(left),
.rightLabel = std::move(right),
},
std::move(limitState),
limitLinePadding);
}
object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text,
Fn<QBrush(QRect)> bg) {
auto result = object_ptr<Ui::FlatLabel>(
parent,
std::move(text),
st::boostLevelBadge);
const auto label = result.data();
label->show();
label->paintRequest() | rpl::on_next([=] {
const auto size = label->textMaxWidth();
const auto rect = QRect(
(label->width() - size) / 2,
st::boostLevelBadge.margin.top(),
size,
st::boostLevelBadge.style.font->height
).marginsAdded(st::boostLevelBadge.margin);
auto p = QPainter(label);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(bg(rect));
p.setPen(Qt::NoPen);
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
const auto &lineFg = st::windowBgRipple;
const auto line = st::boostLevelBadgeLine;
const auto top = st::boostLevelBadge.margin.top()
+ ((st::boostLevelBadge.style.font->height - line) / 2);
const auto left = 0;
const auto skip = st::boostLevelBadgeSkip;
if (const auto right = rect.x() - skip; right > left) {
p.fillRect(left, top, right - left, line, lineFg);
}
const auto right = label->width();
if (const auto left = rect.x() + rect.width() + skip
; left < right) {
p.fillRect(left, top, right - left, line, lineFg);
}
}, label->lifetime());
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,154 @@
/*
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 "base/object_ptr.h"
namespace Ui {
void StartFireworks(not_null<QWidget*> parent);
class Show;
class RpWidget;
class GenericBox;
class VerticalLayout;
class FlatLabel;
struct BoostCounters {
int level = 0;
int boosts = 0;
int thisLevelBoosts = 0;
int nextLevelBoosts = 0; // Zero means no next level is available.
int mine = 0;
friend inline constexpr bool operator==(
BoostCounters,
BoostCounters) = default;
};
struct BoostFeatures {
base::flat_map<int, int> nameColorsByLevel;
base::flat_map<int, int> linkStylesByLevel;
base::flat_map<int, int> profileColorsByLevel;
int linkLogoLevel = 0;
int profileIconLevel = 0;
int autotranslateLevel = 0;
int transcribeLevel = 0;
int emojiPackLevel = 0;
int emojiStatusLevel = 0;
int wallpaperLevel = 0;
int wallpapersCount = 0;
int customWallpaperLevel = 0;
int sponsoredLevel = 0;
};
struct BoostBoxData {
QString name;
BoostCounters boost;
BoostFeatures features;
int lifting = 0;
bool allowMulti = false;
bool group = false;
};
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(BoostCounters)>)> boost);
void BoostBoxAlready(not_null<GenericBox*> box, bool group);
void GiftForBoostsBox(
not_null<GenericBox*> box,
QString channel,
int receive,
bool again);
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group);
void PremiumForBoostsBox(
not_null<GenericBox*> box,
bool group,
Fn<void()> buyPremium);
struct AskBoostChannelColor {
int requiredLevel = 0;
};
struct AskBoostAutotranslate {
int requiredLevel = 0;
};
struct AskBoostWallpaper {
int requiredLevel = 0;
bool group = false;
};
struct AskBoostEmojiStatus {
int requiredLevel = 0;
bool group = false;
};
struct AskBoostEmojiPack {
int requiredLevel = 0;
};
struct AskBoostCustomReactions {
int count = 0;
};
struct AskBoostCpm {
int requiredLevel = 0;
};
struct AskBoostWearCollectible {
int requiredLevel = 0;
};
struct AskBoostReason {
std::variant<
AskBoostChannelColor,
AskBoostAutotranslate,
AskBoostWallpaper,
AskBoostEmojiStatus,
AskBoostEmojiPack,
AskBoostCustomReactions,
AskBoostCpm,
AskBoostWearCollectible> data;
};
struct AskBoostBoxData {
QString link;
BoostCounters boost;
BoostFeatures features;
AskBoostReason reason;
bool group = false;
};
void AskBoostBox(
not_null<GenericBox*> box,
AskBoostBoxData data,
Fn<void()> openStatistics,
Fn<void()> startGiveaway);
[[nodiscard]] object_ptr<RpWidget> MakeLinkLabel(
not_null<QWidget*> parent,
rpl::producer<QString> text,
rpl::producer<QString> link,
std::shared_ptr<Show> show,
object_ptr<RpWidget> right);
void FillBoostLimit(
rpl::producer<> showFinished,
not_null<VerticalLayout*> container,
rpl::producer<BoostCounters> data,
style::margins limitLinePadding);
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text,
Fn<QBrush(QRect)> bg);
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
/*
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 "ui/layers/box_content.h"
#include "ui/widgets/tooltip.h"
#include "base/required.h"
#include "base/timer.h"
#include <QtCore/QDate>
namespace style {
struct CalendarSizes;
struct CalendarColors;
} // namespace style
namespace st {
extern const style::CalendarSizes &defaultCalendarSizes;
extern const style::CalendarColors &defaultCalendarColors;
} // namespace st
namespace Ui {
class IconButton;
class ScrollArea;
class CalendarBox;
struct CalendarBoxArgs {
template <typename T>
using required = base::required<T>;
required<QDate> month;
required<QDate> highlighted;
required<Fn<void(QDate date)>> callback;
FnMut<void(not_null<CalendarBox*>)> finalize;
const style::CalendarSizes &st = st::defaultCalendarSizes;
QDate minDate;
QDate maxDate;
bool allowsSelection = false;
Fn<void(
not_null<Ui::CalendarBox*>,
std::optional<int>)> selectionChanged;
const style::CalendarColors &stColors = st::defaultCalendarColors;
};
class CalendarBox final : public BoxContent, private AbstractTooltipShower {
public:
CalendarBox(QWidget*, CalendarBoxArgs &&args);
~CalendarBox();
void toggleSelectionMode(bool enabled);
[[nodiscard]] QDate selectedFirstDate() const;
[[nodiscard]] QDate selectedLastDate() const;
protected:
void prepare() override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void monthChanged(QDate month);
bool isPreviousEnabled() const;
bool isNextEnabled() const;
void goPreviousMonth();
void goNextMonth();
void setExactScroll();
void processScroll();
void createButtons();
void showJumpTooltip(not_null<IconButton*> button);
void jumpAfterDelay(not_null<IconButton*> button);
void jump(QPointer<IconButton> button);
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
const style::CalendarSizes &_st;
const style::CalendarColors &_styleColors;
class Context;
std::unique_ptr<Context> _context;
std::unique_ptr<ScrollArea> _scroll;
class Inner;
not_null<Inner*> _inner;
class FloatingDate;
std::unique_ptr<FloatingDate> _floatingDate;
class Title;
object_ptr<Title> _title;
object_ptr<IconButton> _previous;
object_ptr<IconButton> _next;
bool _previousEnabled = false;
bool _nextEnabled = false;
Fn<void(QDate date)> _callback;
FnMut<void(not_null<CalendarBox*>)> _finalize;
bool _watchScroll = false;
QPointer<IconButton> _tooltipButton;
QPointer<IconButton> _jumpButton;
base::Timer _jumpTimer;
bool _selectionMode = false;
Fn<void(
not_null<Ui::CalendarBox*>,
std::optional<int>)> _selectionChanged;
};
} // namespace Ui

View File

@@ -0,0 +1,341 @@
/*
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 "ui/boxes/choose_date_time.h"
#include "base/unixtime.h"
#include "base/event_filter.h"
#include "ui/boxes/calendar_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/time_input.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QtWidgets/QTextEdit>
namespace Ui {
namespace {
constexpr auto kMinimalSchedule = TimeId(10);
QString DayString(const QDate &date) {
return tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthDay(date.month())(tr::now),
lt_day,
QString::number(date.day()));
}
QString TimeString(QTime time) {
return QString("%1:%2"
).arg(time.hour()
).arg(time.minute(), 2, 10, QLatin1Char('0'));
}
} // namespace
ChooseDateTimeStyleArgs::ChooseDateTimeStyleArgs()
: labelStyle(&st::boxLabel)
, dateFieldStyle(&st::scheduleDateField)
, timeFieldStyle(&st::scheduleTimeField)
, separatorStyle(&st::scheduleTimeSeparator)
, atStyle(&st::scheduleAtLabel)
, calendarStyle(&st::defaultCalendarColors) {
}
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<GenericBox*> box,
ChooseDateTimeBoxArgs &&args) {
struct State {
rpl::variable<QDate> date;
rpl::variable<int> width;
not_null<InputField*> day;
not_null<TimeInput*> time;
not_null<FlatLabel*> at;
};
box->setTitle(std::move(args.title));
box->setWidth(st::boxWideWidth);
const auto content = box->addRow(
object_ptr<FixedHeightWidget>(box, st::scheduleHeight),
style::al_top);
if (args.description) {
box->addRow(object_ptr<FlatLabel>(
box,
std::move(args.description),
*args.style.labelStyle));
}
const auto parsed = base::unixtime::parse(args.time);
const auto state = box->lifetime().make_state<State>(State{
.date = parsed.date(),
.day = CreateChild<InputField>(
content,
*args.style.dateFieldStyle),
.time = CreateChild<TimeInput>(
content,
TimeString(parsed.time()),
*args.style.timeFieldStyle,
*args.style.dateFieldStyle,
*args.style.separatorStyle,
st::scheduleTimeSeparatorPadding),
.at = CreateChild<FlatLabel>(
content,
tr::lng_schedule_at(),
*args.style.atStyle),
});
state->date.value(
) | rpl::on_next([=](QDate date) {
state->day->setText(DayString(date));
state->time->setFocusFast();
}, state->day->lifetime());
const auto min = args.min ? args.min : [] {
return base::unixtime::now() + kMinimalSchedule;
};
const auto max = args.max ? args.max : [] {
return base::unixtime::serialize(
QDateTime::currentDateTime().addYears(1)) - 1;
};
const auto minDate = [=] {
return base::unixtime::parse(min()).date();
};
const auto maxDate = [=] {
return base::unixtime::parse(max()).date();
};
const auto &dayViewport = state->day->rawTextEdit()->viewport();
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Wheel) {
const auto e = static_cast<QWheelEvent*>(event.get());
const auto direction = Ui::WheelDirection(e);
if (!direction) {
return base::EventFilterResult::Continue;
}
const auto d = state->date.current().addDays(direction);
state->date = std::clamp(d, minDate(), maxDate());
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
state->at->widthValue() | rpl::on_next([=](int width) {
const auto full = st::scheduleDateWidth
+ st::scheduleAtSkip
+ width
+ st::scheduleAtSkip
+ st::scheduleTimeWidth;
content->setNaturalWidth(full);
state->width = full;
}, state->at->lifetime());
content->widthValue(
) | rpl::on_next([=](int width) {
const auto paddings = width
- state->at->width()
- 2 * st::scheduleAtSkip
- st::scheduleDateWidth
- st::scheduleTimeWidth;
const auto left = paddings / 2;
state->day->resizeToWidth(st::scheduleDateWidth);
state->day->moveToLeft(left, st::scheduleDateTop, width);
state->at->moveToLeft(
left + st::scheduleDateWidth + st::scheduleAtSkip,
st::scheduleAtTop,
width);
state->time->resizeToWidth(st::scheduleTimeWidth);
state->time->moveToLeft(
width - left - st::scheduleTimeWidth,
st::scheduleDateTop,
width);
}, content->lifetime());
const auto calendar
= content->lifetime().make_state<base::weak_qptr<CalendarBox>>();
const auto calendarStyle = args.style.calendarStyle;
state->day->focusedChanges(
) | rpl::on_next([=](bool focused) {
if (*calendar || !focused) {
return;
}
*calendar = box->getDelegate()->show(
Box<CalendarBox>(Ui::CalendarBoxArgs{
.month = state->date.current(),
.highlighted = state->date.current(),
.callback = crl::guard(box, [=](QDate chosen) {
state->date = chosen;
(*calendar)->closeBox();
}),
.minDate = minDate(),
.maxDate = maxDate(),
.stColors = *calendarStyle,
}));
(*calendar)->boxClosing(
) | rpl::on_next(crl::guard(state->time, [=] {
state->time->setFocusFast();
}), (*calendar)->lifetime());
}, state->day->lifetime());
const auto collect = [=] {
const auto timeValue = state->time->valueCurrent().split(':');
if (timeValue.size() != 2) {
return 0;
}
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
if (!time.isValid()) {
return 0;
}
const auto result = base::unixtime::serialize(
QDateTime(state->date.current(), time));
if (result < min() || result > max()) {
return 0;
}
return result;
};
const auto save = [=, done = args.done] {
if (const auto result = collect()) {
done(result);
} else {
state->time->showError();
}
};
state->time->submitRequests(
) | rpl::on_next(save, state->time->lifetime());
auto result = ChooseDateTimeBoxDescriptor();
box->setFocusCallback([=] { state->time->setFocusFast(); });
result.width = state->width.value();
result.submit = box->addButton(std::move(args.submit), save);
result.collect = [=] {
if (const auto result = collect()) {
return result;
}
state->time->showError();
return 0;
};
result.values = rpl::combine(
state->date.value(),
state->time->value()
) | rpl::map(collect);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
return result;
}
object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
not_null<Ui::RpWidget*> parent,
ChooseRepeatPeriodArgs &&args) {
auto result = object_ptr<Ui::RpWidget>(parent.get());
const auto raw = result.data();
struct Entry {
TimeId value = 0;
QString text;
};
auto map = std::vector<Entry>{
{ 0, tr::lng_schedule_repeat_never(tr::now) },
{ 24 * 60 * 60, tr::lng_schedule_repeat_daily(tr::now) },
{ 7 * 24 * 60 * 60, tr::lng_schedule_repeat_weekly(tr::now) },
{ 14 * 24 * 60 * 60, tr::lng_schedule_repeat_biweekly(tr::now) },
{ 30 * 24 * 60 * 60, tr::lng_schedule_repeat_monthly(tr::now) },
{
91 * 24 * 60 * 60,
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 3)
},
{
182 * 24 * 60 * 60,
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 6)
},
{ 365 * 24 * 60 * 60, tr::lng_schedule_repeat_yearly(tr::now) },
};
if (args.test) {
map.insert(begin(map) + 1, Entry{ 300, u"Every 5 minutes"_q });
map.insert(begin(map) + 1, Entry{ 60, u"Every minute"_q });
}
const auto label = Ui::CreateChild<Ui::FlatLabel>(raw, QString());
rpl::combine(
raw->widthValue(),
label->naturalWidthValue()
) | rpl::on_next([=](int outer, int natural) {
label->resizeToWidth(std::min(outer, natural));
}, raw->lifetime());
label->heightValue() | rpl::on_next([=](int height) {
raw->resize(raw->width(), height);
}, label->lifetime());
struct State {
rpl::variable<TimeId> value;
rpl::variable<bool> locked;
std::unique_ptr<Ui::PopupMenu> menu;
};
const auto state = raw->lifetime().make_state<State>(State{
.value = args.value,
.locked = std::move(args.locked),
});
rpl::combine(
state->value.value(),
state->locked.value()
) | rpl::on_next([=](TimeId value, bool locked) {
auto result = tr::lng_schedule_repeat_label(
tr::now,
tr::marked);
const auto text = [&] {
const auto i = ranges::lower_bound(
map,
value,
ranges::less{},
&Entry::value);
return (i != end(map)) ? i->text : map.back().text;
}();
label->setMarkedText(result.append(' ').append(tr::link(
tr::bold(text).append(
Ui::Text::IconEmoji(locked
? &st::scheduleRepeatDropdownLock
: &st::scheduleRepeatDropdownArrow))
)));
return result;
}, label->lifetime());
label->setClickHandlerFilter([=](const auto &...) {
if (args.filter && args.filter()) {
return false;
}
const auto changed = args.changed;
state->menu = std::make_unique<Ui::PopupMenu>(label);
const auto menu = state->menu.get();
menu->setDestroyedCallback(crl::guard(label, [=] {
if (state->menu.get() == menu) {
state->menu.release();
}
}));
for (const auto &entry : map) {
const auto value = entry.value;
menu->addAction(entry.text, [=] {
state->value = value;
changed(value);
});
}
menu->popup(QCursor::pos());
return false;
});
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,66 @@
/*
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 "ui/layers/generic_box.h"
namespace style {
struct FlatLabel;
struct InputField;
struct CalendarColors;
} // namespace style
namespace Ui {
class RoundButton;
struct ChooseDateTimeBoxDescriptor {
QPointer<RoundButton> submit;
Fn<TimeId()> collect;
rpl::producer<TimeId> values;
rpl::producer<int> width;
};
struct ChooseDateTimeStyleArgs {
ChooseDateTimeStyleArgs();
const style::FlatLabel *labelStyle;
const style::InputField *dateFieldStyle;
const style::InputField *timeFieldStyle;
const style::FlatLabel *separatorStyle;
const style::FlatLabel *atStyle;
const style::CalendarColors *calendarStyle;
};
struct ChooseDateTimeBoxArgs {
rpl::producer<QString> title;
rpl::producer<QString> submit;
Fn<void(TimeId)> done;
Fn<TimeId()> min;
TimeId time = 0;
Fn<TimeId()> max;
rpl::producer<QString> description;
ChooseDateTimeStyleArgs style;
};
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<GenericBox*> box,
ChooseDateTimeBoxArgs &&args);
struct ChooseRepeatPeriodArgs {
TimeId value = 0;
rpl::variable<bool> locked;
Fn<bool()> filter;
Fn<void(TimeId)> changed;
bool test = false;
};
[[nodiscard]] object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
not_null<Ui::RpWidget*> parent,
ChooseRepeatPeriodArgs &&args);
} // namespace Ui

View File

@@ -0,0 +1,967 @@
/*
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 "ui/boxes/choose_font_box.h"
#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/style/style_core_font.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"
#include <QtGui/QFontDatabase>
namespace Ui {
namespace {
constexpr auto kMinTextWidth = 120;
constexpr auto kMaxTextWidth = 320;
constexpr auto kMaxTextLines = 3;
struct PreviewRequest {
QString family;
QColor msgBg;
QColor msgShadow;
QColor replyBar;
QColor replyNameFg;
QColor textFg;
QImage bubbleTail;
};
class PreviewPainter {
public:
PreviewPainter(const QImage &bg, PreviewRequest request);
QImage takeResult();
private:
void layout();
void paintBubble(Painter &p);
void paintContent(Painter &p);
void paintReply(Painter &p);
void paintMessage(Painter &p);
void validateBubbleCache();
const PreviewRequest _request;
const style::owned_color _msgBg;
const style::owned_color _msgShadow;
style::owned_font _nameFontOwned;
style::font _nameFont;
style::TextStyle _nameStyle;
style::owned_font _textFontOwned;
style::font _textFont;
style::TextStyle _textStyle;
Ui::Text::String _nameText;
Ui::Text::String _replyText;
Ui::Text::String _messageText;
QRect _replyRect;
QRect _name;
QRect _reply;
QRect _message;
QRect _content;
QRect _bubble;
QSize _outer;
Ui::CornersPixmaps _bubbleCorners;
QPixmap _bubbleShadowBottomRight;
QImage _result;
};
class Selector final : public Ui::RpWidget {
public:
Selector(
not_null<QWidget*> parent,
const QString &now,
rpl::producer<QString> filter,
rpl::producer<> submits,
Fn<void(QString)> chosen,
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo);
void initScroll(anim::type animated);
void setMinHeight(int height);
void selectSkip(Qt::Key direction);
private:
struct Entry {
QString id;
QString key;
QString text;
QStringList keywords;
QImage cache;
std::unique_ptr<Ui::RadioView> check;
std::unique_ptr<Ui::RippleAnimation> ripple;
int paletteVersion = 0;
};
[[nodiscard]] static std::vector<Entry> FullList(const QString &now);
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
[[nodiscard]] bool searching() const;
[[nodiscard]] int shownRowsCount() const;
[[nodiscard]] Entry &shownRowAt(int index);
void applyFilter(const QString &query);
void updateSelected(int selected);
void updatePressed(int pressed);
void updateRow(int index);
void updateRow(not_null<Entry*> row, int hint);
void addRipple(int index, QPoint position);
void validateCache(Entry &row);
void choose(Entry &row);
const style::SettingsButton &_st;
std::vector<Entry> _rows;
std::vector<not_null<Entry*>> _filtered;
QString _chosen;
int _selected = -1;
int _pressed = -1;
std::optional<QPoint> _lastGlobalPoint;
bool _selectedByKeyboard = false;
Fn<void(QString)> _callback;
Fn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;
int _rowsSkip = 0;
int _rowHeight = 0;
int _minHeight = 0;
QString _query;
QStringList _queryWords;
rpl::lifetime _lifetime;
};
Selector::Selector(
not_null<QWidget*> parent,
const QString &now,
rpl::producer<QString> filter,
rpl::producer<> submits,
Fn<void(QString)> chosen,
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo)
: RpWidget(parent)
, _st(st::settingsButton)
, _rows(FullList(now))
, _chosen(now)
, _callback(std::move(chosen))
, _scrollTo(std::move(scrollTo))
, _rowsSkip(st::settingsInfoPhotoSkip)
, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {
setMouseTracking(true);
std::move(filter) | rpl::on_next([=](const QString &query) {
applyFilter(query);
}, _lifetime);
std::move(
submits
) | rpl::on_next([=] {
if (_selected >= 0) {
choose(shownRowAt(_selected));
} else if (searching() && !_filtered.empty()) {
choose(*_filtered.front());
}
}, _lifetime);
}
void Selector::applyFilter(const QString &query) {
if (_query == query) {
return;
}
_query = query;
updateSelected(-1);
updatePressed(-1);
_queryWords = TextUtilities::PrepareSearchWords(_query);
const auto skip = [](
const QStringList &haystack,
const QStringList &needles) {
const auto find = [](
const QStringList &haystack,
const QString &needle) {
for (const auto &item : haystack) {
if (item.startsWith(needle)) {
return true;
}
}
return false;
};
for (const auto &needle : needles) {
if (!find(haystack, needle)) {
return true;
}
}
return false;
};
_filtered.clear();
if (!_queryWords.isEmpty()) {
_filtered.reserve(_rows.size());
for (auto &row : _rows) {
if (!skip(row.keywords, _queryWords)) {
_filtered.push_back(&row);
} else {
row.ripple = nullptr;
}
}
}
resizeToWidth(width());
Ui::SendPendingMoveResizeEvents(this);
update();
}
void Selector::updateSelected(int selected) {
if (_selected == selected) {
return;
}
const auto was = (_selected >= 0);
updateRow(_selected);
_selected = selected;
updateRow(_selected);
const auto now = (_selected >= 0);
if (was != now) {
setCursor(now ? style::cur_pointer : style::cur_default);
}
if (_selectedByKeyboard) {
const auto top = (_selected > 0)
? (_rowsSkip + _selected * _rowHeight)
: 0;
const auto bottom = (_selected > 0)
? (top + _rowHeight)
: _selected
? 0
: _rowHeight;
_scrollTo({ top, bottom }, anim::type::instant);
}
}
void Selector::updatePressed(int pressed) {
if (_pressed == pressed) {
return;
} else if (_pressed >= 0) {
if (auto &ripple = shownRowAt(_pressed).ripple) {
ripple->lastStop();
}
}
updateRow(_pressed);
_pressed = pressed;
updateRow(_pressed);
}
void Selector::updateRow(int index) {
if (index >= 0) {
update(0, _rowsSkip + index * _rowHeight, width(), _rowHeight);
}
}
void Selector::updateRow(not_null<Entry*> row, int hint) {
if (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) {
updateRow(hint);
} else if (searching()) {
const auto i = ranges::find(_filtered, row);
if (i != end(_filtered)) {
updateRow(int(i - begin(_filtered)));
}
} else {
const auto index = int(row.get() - &_rows[0]);
Assert(index >= 0 && index < _rows.size());
updateRow(index);
}
}
void Selector::validateCache(Entry &row) {
const auto version = style::PaletteVersion();
if (row.cache.isNull()) {
const auto ratio = style::DevicePixelRatio();
row.cache = QImage(
QSize(width(), _rowHeight) * ratio,
QImage::Format_ARGB32_Premultiplied);
row.cache.setDevicePixelRatio(ratio);
} else if (row.paletteVersion == version) {
return;
}
row.paletteVersion = version;
row.cache.fill(Qt::transparent);
auto owned = style::owned_font(row.id, 0, st::boxFontSize);
const auto font = owned.font();
auto p = QPainter(&row.cache);
p.setFont(font);
p.setPen(st::windowFg);
const auto textw = width() - _st.padding.left() - _st.padding.right();
const auto textt = (_rowHeight - font->height) / 2.;
p.drawText(
_st.padding.left(),
textt + font->ascent,
font->elided(row.text, textw));
}
bool Selector::searching() const {
return !_queryWords.isEmpty();
}
int Selector::shownRowsCount() const {
return searching() ? int(_filtered.size()) : int(_rows.size());
}
Selector::Entry &Selector::shownRowAt(int index) {
return searching() ? *_filtered[index] : _rows[index];
}
void Selector::setMinHeight(int height) {
_minHeight = height;
if (_minHeight > 0) {
resizeToWidth(width());
}
}
void Selector::selectSkip(Qt::Key key) {
const auto count = shownRowsCount();
if (key == Qt::Key_Down) {
if (_selected + 1 < count) {
_selectedByKeyboard = true;
updateSelected(_selected + 1);
}
} else if (key == Qt::Key_Up) {
if (_selected >= 0) {
_selectedByKeyboard = true;
updateSelected(_selected - 1);
}
} else if (key == Qt::Key_PageDown) {
const auto change = _minHeight / _rowHeight;
if (_selected + 1 < count) {
_selectedByKeyboard = true;
updateSelected(std::min(_selected + change, count - 1));
}
} else if (key == Qt::Key_PageUp) {
const auto change = _minHeight / _rowHeight;
if (_selected > 0) {
_selectedByKeyboard = true;
updateSelected(std::max(_selected - change, 0));
} else if (!_selected) {
_selectedByKeyboard = true;
updateSelected(-1);
}
}
}
void Selector::initScroll(anim::type animated) {
const auto index = [&] {
if (searching()) {
const auto i = ranges::find(_filtered, _chosen, &Entry::id);
if (i != end(_filtered)) {
return int(i - begin(_filtered));
}
return -1;
}
const auto i = ranges::find(_rows, _chosen, &Entry::id);
Assert(i != end(_rows));
return int(i - begin(_rows));
}();
if (index >= 0) {
const auto top = _rowsSkip + index * _rowHeight;
const auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0);
_scrollTo({ use, use + _minHeight }, animated);
}
}
int Selector::resizeGetHeight(int newWidth) {
const auto added = 2 * _rowsSkip;
return std::max(added + shownRowsCount() * _rowHeight, _minHeight);
}
void Selector::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto rows = shownRowsCount();
if (!rows) {
p.setFont(st::normalFont);
p.setPen(st::windowSubTextFg);
p.drawText(
QRect(0, 0, width(), height() * 2 / 3),
tr::lng_font_not_found(tr::now),
style::al_center);
return;
}
const auto clip = e->rect();
const auto clipped = std::max(clip.y() - _rowsSkip, 0);
const auto from = std::min(clipped / _rowHeight, rows);
const auto till = std::min(
(clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight,
rows);
const auto active = (_pressed >= 0) ? _pressed : _selected;
for (auto i = from; i != till; ++i) {
auto &row = shownRowAt(i);
const auto y = _rowsSkip + i * _rowHeight;
const auto bg = (i == active) ? st::windowBgOver : st::windowBg;
const auto rect = QRect(0, y, width(), _rowHeight);
p.fillRect(rect, bg);
if (row.ripple) {
row.ripple->paint(p, 0, y, width());
if (row.ripple->empty()) {
row.ripple = nullptr;
}
}
validateCache(row);
p.drawImage(0, y, row.cache);
if (!row.check) {
row.check = std::make_unique<Ui::RadioView>(
st::langsRadio,
(row.id == _chosen),
[=, row = &row] { updateRow(row, i); });
}
row.check->paint(
p,
_st.iconLeft,
y + (_rowHeight - st::langsRadio.diameter) / 2,
width());
}
}
void Selector::leaveEventHook(QEvent *e) {
_lastGlobalPoint = std::nullopt;
if (!_selectedByKeyboard) {
updateSelected(-1);
}
}
void Selector::mouseMoveEvent(QMouseEvent *e) {
if (!_lastGlobalPoint) {
_lastGlobalPoint = e->globalPos();
if (_selectedByKeyboard) {
return;
}
} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {
return;
} else {
_lastGlobalPoint = e->globalPos();
}
_selectedByKeyboard = false;
const auto y = e->y() - _rowsSkip;
const auto index = (y >= 0) ? (y / _rowHeight) : -1;
updateSelected((index >= 0 && index < shownRowsCount()) ? index : -1);
}
void Selector::mousePressEvent(QMouseEvent *e) {
updatePressed(_selected);
if (_pressed >= 0) {
addRipple(_pressed, e->pos());
}
}
void Selector::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = _pressed;
updatePressed(-1);
if (pressed >= 0 && pressed == _selected) {
choose(shownRowAt(pressed));
}
}
void Selector::choose(Entry &row) {
const auto id = row.id;
if (_chosen != id) {
const auto i = ranges::find(_rows, _chosen, &Entry::id);
Assert(i != end(_rows));
if (i->check) {
i->check->setChecked(false, anim::type::normal);
}
_chosen = id;
if (row.check) {
row.check->setChecked(true, anim::type::normal);
}
}
const auto animated = searching()
? anim::type::instant
: anim::type::normal;
_callback(id);
initScroll(animated);
}
void Selector::addRipple(int index, QPoint position) {
Expects(index >= 0 && index < shownRowsCount());
const auto row = &shownRowAt(index);
if (!row->ripple) {
row->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RectMask({ width(), _rowHeight }),
[=] { updateRow(row, index); });
}
row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));
}
std::vector<Selector::Entry> Selector::FullList(const QString &now) {
using namespace TextUtilities;
auto database = QFontDatabase();
auto families = database.families();
auto result = std::vector<Entry>();
result.reserve(families.size() + 3);
const auto add = [&](const QString &text, const QString &id = {}) {
result.push_back({
.id = id,
.text = text,
.keywords = PrepareSearchWords(text),
});
};
add(tr::lng_font_default(tr::now));
add(tr::lng_font_system(tr::now), style::SystemFontTag());
for (const auto &family : families) {
if (database.isScalable(family)) {
result.push_back({ .id = family });
}
}
auto nowIt = ranges::find(result, now, &Entry::id);
if (nowIt == end(result)) {
result.push_back({ .id = now });
nowIt = end(result) - 1;
}
for (auto i = begin(result) + 2; i != end(result); ++i) {
i->key = TextUtilities::RemoveAccents(i->id).toLower();
i->text = i->id;
i->keywords = TextUtilities::PrepareSearchWords(i->id);
}
auto skip = 2;
if (nowIt - begin(result) >= skip) {
std::swap(result[2], *nowIt);
++skip;
}
ranges::sort(
begin(result) + skip, end(result),
std::less<>(),
&Entry::key);
return result;
}
[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) {
return {
.family = family,
.msgBg = st::msgInBg->c,
.msgShadow = st::msgInShadow->c,
.replyBar = st::msgInReplyBarColor->c,
.replyNameFg = st::msgInServiceFg->c,
.textFg = st::historyTextInFg->c,
.bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c),
};
}
PreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request)
: _request(request)
, _msgBg(_request.msgBg)
, _msgShadow(_request.msgShadow)
, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)
, _nameFont(_nameFontOwned.font())
, _nameStyle(st::semiboldTextStyle)
, _textFontOwned(_request.family, 0, st::fsize)
, _textFont(_textFontOwned.font())
, _textStyle(st::defaultTextStyle) {
_nameStyle.font = _nameFont;
_textStyle.font = _textFont;
layout();
const auto ratio = style::DevicePixelRatio();
_result = QImage(
_outer * ratio,
QImage::Format_ARGB32_Premultiplied);
_result.setDevicePixelRatio(ratio);
auto p = Painter(&_result);
p.drawImage(0, 0, bg);
p.translate(_bubble.topLeft());
paintBubble(p);
}
void PreviewPainter::paintBubble(Painter &p) {
validateBubbleCache();
const auto bubble = QRect(QPoint(), _bubble.size());
const auto cornerShadow = _bubbleShadowBottomRight.size()
/ _bubbleShadowBottomRight.devicePixelRatio();
p.drawPixmap(
bubble.width() - cornerShadow.width(),
bubble.height() + st::msgShadow - cornerShadow.height(),
_bubbleShadowBottomRight);
Ui::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners);
const auto &bubbleTail = _request.bubbleTail;
const auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio();
p.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail);
p.fillRect(
-tail.width(),
bubble.height(),
tail.width() + bubble.width() - cornerShadow.width(),
st::msgShadow,
_request.msgShadow);
p.translate(_content.topLeft());
const auto local = _content.translated(-_content.topLeft());
p.setClipRect(local);
paintContent(p);
}
void PreviewPainter::validateBubbleCache() {
if (!_bubbleCorners.p[0].isNull()) {
return;
}
const auto radius = st::bubbleRadiusLarge;
_bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color());
_bubbleCorners.p[2] = {};
_bubbleShadowBottomRight
= Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3];
}
void PreviewPainter::paintContent(Painter &p) {
paintReply(p);
p.translate(_message.topLeft());
const auto local = _message.translated(-_message.topLeft());
p.setClipRect(local);
paintMessage(p);
}
void PreviewPainter::paintReply(Painter &p) {
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(_request.replyBar);
const auto outline = st::messageTextStyle.blockquote.outline;
const auto radius = st::messageTextStyle.blockquote.radius;
p.setOpacity(Ui::kDefaultOutline1Opacity);
p.setClipRect(
_replyRect.x(),
_replyRect.y(),
outline,
_replyRect.height());
p.drawRoundedRect(_replyRect, radius, radius);
p.setOpacity(Ui::kDefaultBgOpacity);
p.setClipRect(
_replyRect.x() + outline,
_replyRect.y(),
_replyRect.width() - outline,
_replyRect.height());
p.drawRoundedRect(_replyRect, radius, radius);
}
p.setOpacity(1.);
p.setClipping(false);
p.setPen(_request.replyNameFg);
_nameText.drawLeftElided(
p,
_name.x(),
_name.y(),
_name.width(),
_outer.width());
p.setPen(_request.textFg);
_replyText.drawLeftElided(
p,
_reply.x(),
_reply.y(),
_reply.width(),
_outer.width());
}
void PreviewPainter::paintMessage(Painter &p) {
p.setPen(_request.textFg);
_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());
}
QImage PreviewPainter::takeResult() {
return std::move(_result);
}
void PreviewPainter::layout() {
const auto skip = st::boxRowPadding.left();
const auto minTextWidth = style::ConvertScale(kMinTextWidth);
const auto maxTextWidth = st::boxWidth
- 2 * skip
- st::msgPadding.left()
- st::msgPadding.right();
_nameText = Ui::Text::String(
_nameStyle,
tr::lng_settings_chat_message_reply_from(tr::now));
_replyText = Ui::Text::String(
_textStyle,
tr::lng_background_text2(tr::now));
_messageText = Ui::Text::String(
_textStyle,
tr::lng_background_text1(tr::now),
kDefaultTextOptions,
st::msgMinWidth / 2);
const auto namePosition = QPoint(
st::historyReplyPadding.left(),
st::historyReplyPadding.top());
const auto replyPosition = QPoint(
st::historyReplyPadding.left(),
(st::historyReplyPadding.top() + _nameFont->height));
const auto paddingRight = st::historyReplyPadding.right();
const auto wantedWidth = std::max({
namePosition.x() + _nameText.maxWidth() + paddingRight,
replyPosition.x() + _replyText.maxWidth() + paddingRight,
_messageText.maxWidth()
});
const auto messageWidth = std::clamp(
wantedWidth,
minTextWidth,
maxTextWidth);
const auto messageHeight = _messageText.countHeight(messageWidth);
_replyRect = QRect(
st::msgReplyBarPos.x(),
st::historyReplyTop,
messageWidth,
(st::historyReplyPadding.top()
+ _nameFont->height
+ _textFont->height
+ st::historyReplyPadding.bottom()));
_name = QRect(
_replyRect.topLeft() + namePosition,
QSize(messageWidth - namePosition.x(), _nameFont->height));
_reply = QRect(
_replyRect.topLeft() + replyPosition,
QSize(messageWidth - replyPosition.x(), _textFont->height));
_message = QRect(0, 0, messageWidth, messageHeight);
const auto replySkip = _replyRect.y()
+ _replyRect.height()
+ st::historyReplyBottom;
_message.moveTop(replySkip);
_content = QRect(0, 0, messageWidth, replySkip + messageHeight);
const auto msgPadding = st::msgPadding;
_bubble = _content.marginsAdded(msgPadding);
_content.moveTopLeft(-_bubble.topLeft());
_bubble.moveTopLeft({});
_outer = QSize(st::boxWidth, st::boxWidth / 2);
_bubble.moveTopLeft({ skip, std::max(
(_outer.height() - _bubble.height()) / 2,
st::msgMargin.top()) });
}
[[nodiscard]] QImage GeneratePreview(
const QImage &bg,
PreviewRequest request) {
return PreviewPainter(bg, request).takeResult();
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakePreview(
not_null<QWidget*> parent,
Fn<QImage()> generatePreviewBg,
rpl::producer<QString> family) {
auto result = object_ptr<Ui::RpWidget>(parent.get());
const auto raw = result.data();
struct State {
QImage preview;
QImage bg;
QString family;
};
const auto state = raw->lifetime().make_state<State>();
state->bg = generatePreviewBg();
style::PaletteChanged() | rpl::on_next([=] {
state->bg = generatePreviewBg();
}, raw->lifetime());
rpl::combine(
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged()),
std::move(family)
) | rpl::on_next([=](const auto &, QString family) {
state->family = family;
if (state->preview.isNull()) {
state->preview = GeneratePreview(
state->bg,
PrepareRequest(family));
const auto ratio = state->preview.devicePixelRatio();
raw->resize(state->preview.size() / int(ratio));
} else {
const auto weak = base::make_weak(raw);
const auto request = PrepareRequest(family);
crl::async([=, bg = state->bg] {
crl::on_main([
weak,
state,
preview = GeneratePreview(bg, request)
]() mutable {
if (const auto strong = weak.get()) {
state->preview = std::move(preview);
const auto ratio = state->preview.devicePixelRatio();
strong->resize(
strong->width(),
(state->preview.height() / int(ratio)));
strong->update();
}
});
});
}
}, raw->lifetime());
raw->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(raw).drawImage(0, 0, state->preview);
}, raw->lifetime());
return result;
}
} // namespace
void ChooseFontBox(
not_null<GenericBox*> box,
Fn<QImage()> generatePreviewBg,
const QString &family,
Fn<void(QString)> save) {
box->setTitle(tr::lng_font_box_title());
struct State {
rpl::variable<QString> family;
rpl::variable<QString> query;
rpl::event_stream<> submits;
};
const auto state = box->lifetime().make_state<State>(State{
.family = family,
});
const auto top = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box));
top->add(MakePreview(top, generatePreviewBg, state->family.value()));
const auto filter = top->add(object_ptr<Ui::MultiSelect>(
top,
st::defaultMultiSelect,
tr::lng_participant_filter()));
top->resizeToWidth(st::boxWidth);
filter->setSubmittedCallback([=](Qt::KeyboardModifiers) {
state->submits.fire({});
});
filter->setQueryChangedCallback([=](const QString &query) {
state->query = query;
});
filter->setCancelledCallback([=] {
filter->clearQuery();
});
const auto chosen = [=](const QString &value) {
state->family = value;
filter->clearQuery();
};
const auto scrollTo = [=](
Ui::ScrollToRequest request,
anim::type animated) {
box->scrollTo(request, animated);
};
const auto selector = box->addRow(
object_ptr<Selector>(
box,
state->family.current(),
state->query.value(),
state->submits.events(),
chosen,
scrollTo),
QMargins());
box->setMinHeight(st::boxMaxListHeight);
box->setMaxHeight(st::boxMaxListHeight);
base::install_event_filter(filter, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto key = static_cast<QKeyEvent*>(e.get())->key();
if (key == Qt::Key_Up
|| key == Qt::Key_Down
|| key == Qt::Key_PageUp
|| key == Qt::Key_PageDown) {
selector->selectSkip(Qt::Key(key));
return base::EventFilterResult::Cancel;
}
}
return base::EventFilterResult::Continue;
});
rpl::combine(
box->heightValue(),
top->heightValue()
) | rpl::on_next([=](int box, int top) {
selector->setMinHeight(box - top);
}, selector->lifetime());
const auto apply = [=](QString chosen) {
if (chosen == family) {
box->closeBox();
return;
}
box->getDelegate()->show(Ui::MakeConfirmBox({
.text = tr::lng_settings_need_restart(),
.confirmed = [=] { save(chosen); },
.confirmText = tr::lng_settings_restart_now(),
}));
};
const auto refreshButtons = [=](QString chosen) {
box->clearButtons();
// Doesn't fit in most languages.
//if (!chosen.isEmpty()) {
// box->addLeftButton(tr::lng_background_reset_default(), [=] {
// apply(QString());
// });
//}
box->addButton(tr::lng_settings_save(), [=] {
apply(chosen);
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
};
state->family.value(
) | rpl::on_next(refreshButtons, box->lifetime());
box->setFocusCallback([=] {
filter->setInnerFocus();
});
box->setInitScrollCallback([=] {
SendPendingMoveResizeEvents(box);
selector->initScroll(anim::type::instant);
});
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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
namespace Ui {
class GenericBox;
void ChooseFontBox(
not_null<GenericBox*> box,
Fn<QImage()> generatePreviewBg,
const QString &family,
Fn<void(QString)> save);
} // namespace Ui

View File

@@ -0,0 +1,382 @@
/*
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 "ui/boxes/choose_language_box.h"
#include "lang/lang_keys.h"
#include "spellcheck/spellcheck_types.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/multi_select.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "base/debug_log.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
namespace Ui {
namespace {
const auto kLanguageNamePrefix = "cloud_lng_language_";
const auto kTranslateToPrefix = "cloud_lng_translate_to_";
[[nodiscard]] std::vector<LanguageId> TranslationLanguagesList() {
// If adding some languages here you need to check that it is
// supported on the server. Right now server supports those:
//
// 'af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg',
// 'ca', 'ceb', 'zh-CN', 'zh', 'zh-TW', 'co', 'hr', 'cs', 'da', 'nl',
// 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gu',
// 'ht', 'ha', 'haw', 'he', 'iw', 'hi', 'hmn', 'hu', 'is', 'ig', 'id',
// 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'ko', 'ku', 'ky',
// 'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi',
// 'mr', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'ps', 'fa', 'pl', 'pt',
// 'pa', 'ro', 'ru', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si', 'sk',
// 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te',
// 'th', 'tr', 'tk', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi',
// 'yo', 'zu',
return {
{ QLocale::English },
{ QLocale::Arabic },
{ QLocale::Belarusian },
{ QLocale::Catalan },
{ QLocale::Chinese },
{ QLocale::Dutch },
{ QLocale::French },
{ QLocale::German },
{ QLocale::Indonesian },
{ QLocale::Italian },
{ QLocale::Japanese },
{ QLocale::Korean },
{ QLocale::Polish },
{ QLocale::Portuguese },
{ QLocale::Russian },
{ QLocale::Spanish },
{ QLocale::Ukrainian },
{ QLocale::Afrikaans },
{ QLocale::Albanian },
{ QLocale::Amharic },
{ QLocale::Armenian },
{ QLocale::Azerbaijani },
{ QLocale::Basque },
{ QLocale::Bosnian },
{ QLocale::Bulgarian },
{ QLocale::Burmese },
{ QLocale::Croatian },
{ QLocale::Czech },
{ QLocale::Danish },
{ QLocale::Esperanto },
{ QLocale::Estonian },
{ QLocale::Finnish },
{ QLocale::Gaelic },
{ QLocale::Galician },
{ QLocale::Georgian },
{ QLocale::Greek },
{ QLocale::Gusii },
{ QLocale::Hausa },
{ QLocale::Hebrew },
{ QLocale::Hungarian },
{ QLocale::Icelandic },
{ QLocale::Igbo },
{ QLocale::Irish },
{ QLocale::Kazakh },
{ QLocale::Kinyarwanda },
{ QLocale::Kurdish },
{ QLocale::Lao },
{ QLocale::Latvian },
{ QLocale::Lithuanian },
{ QLocale::Luxembourgish },
{ QLocale::Macedonian },
{ QLocale::Malagasy },
{ QLocale::Malay },
{ QLocale::Maltese },
{ QLocale::Maori },
{ QLocale::Mongolian },
{ QLocale::Nepali },
{ QLocale::Pashto },
{ QLocale::Persian },
{ QLocale::Romanian },
{ QLocale::Serbian },
{ QLocale::Shona },
{ QLocale::Sindhi },
{ QLocale::Sinhala },
{ QLocale::Slovak },
{ QLocale::Slovenian },
{ QLocale::Somali },
{ QLocale::Sundanese },
{ QLocale::Swahili },
{ QLocale::Swedish },
{ QLocale::Tajik },
{ QLocale::Tatar },
{ QLocale::Teso },
{ QLocale::Thai },
{ QLocale::Turkish },
{ QLocale::Turkmen },
{ QLocale::Urdu },
{ QLocale::Uzbek },
{ QLocale::Vietnamese },
{ QLocale::Welsh },
{ QLocale::WesternFrisian },
{ QLocale::Xhosa },
{ QLocale::Yiddish },
};
}
class Row final : public SettingsButton {
public:
Row(not_null<RpWidget*> parent, LanguageId id);
[[nodiscard]] bool filtered(const QString &query) const;
[[nodiscard]] LanguageId id() const;
int resizeGetHeight(int newWidth) override;
protected:
void paintEvent(QPaintEvent *e) override;
private:
const style::PeerListItem &_st;
const LanguageId _id;
const QString _status;
const QString _titleText;
Text::String _title;
};
Row::Row(not_null<RpWidget*> parent, LanguageId id)
: SettingsButton(parent, rpl::never<QString>())
, _st(st::inviteLinkListItem)
, _id(id)
, _status(LanguageName(id))
, _titleText(LanguageNameNative(id))
, _title(_st.nameStyle, _titleText) {
}
LanguageId Row::id() const {
return _id;
}
bool Row::filtered(const QString &query) const {
return _status.startsWith(query, Qt::CaseInsensitive)
|| _titleText.startsWith(query, Qt::CaseInsensitive);
}
int Row::resizeGetHeight(int newWidth) {
return _st.height;
}
void Row::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto paintOver = (isOver() || isDown()) && !isDisabled();
SettingsButton::paintBg(p, e->rect(), paintOver);
SettingsButton::paintRipple(p, 0, 0);
SettingsButton::paintToggle(p, width());
const auto &color = st::windowSubTextFg;
p.setPen(Qt::NoPen);
p.setBrush(color);
const auto left = st::defaultSubsectionTitlePadding.left();
const auto toggleRect = SettingsButton::maybeToggleRect();
const auto right = left
+ (toggleRect.isEmpty() ? 0 : (width() - toggleRect.x()));
const auto availableWidth = std::min(
_title.maxWidth(),
width() - left - right);
p.setPen(_st.nameFg);
_title.drawLeft(
p,
left,
_st.namePosition.y(),
availableWidth,
width() - left - right);
p.setPen(paintOver ? _st.statusFgOver : _st.statusFg);
p.setFont(st::contactsStatusFont);
p.drawTextLeft(
left,
_st.statusPosition.y(),
width() - left - right,
_status);
}
} // namespace
QString LanguageNameTranslated(const QString &twoLetterCode) {
return Lang::GetNonDefaultValue(
kLanguageNamePrefix + twoLetterCode.toUtf8());
}
QString LanguageNameLocal(LanguageId id) {
return QLocale::languageToString(id.language());
}
QString LanguageName(LanguageId id) {
const auto translated = LanguageNameTranslated(id.twoLetterCode());
return translated.isEmpty() ? LanguageNameLocal(id) : translated;
}
QString LanguageNameNative(LanguageId id) {
const auto locale = id.locale();
if (locale.language() == QLocale::English
&& (locale.country() == QLocale::UnitedStates
|| locale.country() == QLocale::AnyCountry)) {
return u"English"_q;
} else if (locale.language() == QLocale::Spanish) {
return QString::fromUtf8("\x45\x73\x70\x61\xc3\xb1\x6f\x6c");
} else {
const auto name = locale.nativeLanguageName();
return name.left(1).toUpper() + name.mid(1);
}
}
rpl::producer<QString> TranslateBarTo(LanguageId id) {
const auto translated = Lang::GetNonDefaultValue(
kTranslateToPrefix + id.twoLetterCode().toUtf8());
return (translated.isEmpty()
? tr::lng_translate_bar_to_other
: tr::lng_translate_bar_to)(
lt_name,
rpl::single(translated.isEmpty()
? LanguageNameLocal(id)
: translated));
}
QString TranslateMenuDont(tr::now_t, LanguageId id) {
const auto translated = Lang::GetNonDefaultValue(
kTranslateToPrefix + id.twoLetterCode().toUtf8());
return (translated.isEmpty()
? tr::lng_translate_menu_dont_other
: tr::lng_translate_menu_dont)(
tr::now,
lt_name,
translated.isEmpty() ? LanguageNameLocal(id) : translated);
}
void ChooseLanguageBox(
not_null<GenericBox*> box,
rpl::producer<QString> title,
Fn<void(std::vector<LanguageId>)> callback,
std::vector<LanguageId> selected,
bool multiselect,
Fn<bool(LanguageId)> toggleCheck) {
box->setMinHeight(st::boxWidth);
box->setMaxHeight(st::boxWidth);
box->setTitle(std::move(title));
const auto multiSelect = box->setPinnedToTopContent(
object_ptr<MultiSelect>(
box,
st::defaultMultiSelect,
tr::lng_participant_filter()));
box->setFocusCallback([=] { multiSelect->setInnerFocus(); });
const auto container = box->verticalLayout();
const auto langs = [&] {
auto list = TranslationLanguagesList();
for (const auto id : list) {
LOG(("cloud_lng_language_%1").arg(id.twoLetterCode()));
}
const auto current = LanguageId{ QLocale(
Lang::LanguageIdOrDefault(Lang::Id())).language() };
if (const auto i = ranges::find(list, current); i != end(list)) {
base::reorder(list, std::distance(begin(list), i), 0);
}
ranges::stable_partition(list, [&](LanguageId id) {
return ranges::contains(selected, id);
});
return list;
}();
struct ToggleOne {
LanguageId id;
bool selected = false;
};
struct State {
rpl::event_stream<ToggleOne> toggles;
};
const auto state = box->lifetime().make_state<State>();
auto rows = std::vector<not_null<SlideWrap<Row>*>>();
rows.reserve(langs.size());
for (const auto &id : langs) {
const auto button = container->add(
object_ptr<SlideWrap<Row>>(
container,
object_ptr<Row>(container, id)));
if (multiselect) {
button->entity()->toggleOn(rpl::single(
ranges::contains(selected, id)
) | rpl::then(state->toggles.events(
) | rpl::filter([=](ToggleOne one) {
return one.id == id;
}) | rpl::map([=](ToggleOne one) {
return one.selected;
})));
button->entity()->toggledChanges(
) | rpl::on_next([=](bool value) {
if (toggleCheck && !toggleCheck(id)) {
state->toggles.fire({ .id = id, .selected = !value });
}
}, button->lifetime());
} else {
button->entity()->setClickedCallback([=] {
callback({ id });
box->closeBox();
});
}
rows.push_back(button);
}
multiSelect->setQueryChangedCallback([=](const QString &query) {
for (const auto &row : rows) {
const auto toggled = row->entity()->filtered(query);
if (toggled != row->toggled()) {
row->toggle(toggled, anim::type::instant);
}
}
});
{
const auto label = CreateChild<FlatLabel>(
box.get(),
tr::lng_languages_none(),
st::membersAbout);
box->verticalLayout()->geometryValue(
) | rpl::on_next([=](const QRect &geometry) {
const auto shown = (geometry.height() <= 0);
label->setVisible(shown);
if (shown) {
label->moveToLeft(
(geometry.width() - label->width()) / 2,
geometry.y() + st::membersAbout.style.font->height * 4);
label->stackUnder(box->verticalLayout());
}
}, label->lifetime());
}
if (multiselect) {
box->addButton(tr::lng_settings_save(), [=] {
auto result = ranges::views::all(
rows
) | ranges::views::filter([](const auto &row) {
return row->entity()->toggled();
}) | ranges::views::transform([](const auto &row) {
return row->entity()->id();
}) | ranges::to_vector;
if (!result.empty()) {
callback(std::move(result));
}
box->closeBox();
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Ui

View File

@@ -0,0 +1,36 @@
/*
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
struct LanguageId;
namespace tr {
struct now_t;
} // namespace tr
namespace Ui {
class GenericBox;
[[nodiscard]] QString LanguageNameTranslated(const QString &twoLetterCode);
[[nodiscard]] QString LanguageNameLocal(LanguageId id);
[[nodiscard]] QString LanguageName(LanguageId id);
[[nodiscard]] QString LanguageNameNative(LanguageId id);
[[nodiscard]] rpl::producer<QString> TranslateBarTo(LanguageId id);
[[nodiscard]] QString TranslateMenuDont(tr::now_t, LanguageId id);
void ChooseLanguageBox(
not_null<GenericBox*> box,
rpl::producer<QString> title,
Fn<void(std::vector<LanguageId>)> callback,
std::vector<LanguageId> selected,
bool multiselect,
Fn<bool(LanguageId)> toggleCheck);
} // namespace Ui

View File

@@ -0,0 +1,145 @@
/*
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 "ui/boxes/choose_time.h"
#include "base/qt_signal_producer.h"
#include "ui/ui_utility.h"
#include "ui/widgets/fields/time_part_input_with_placeholder.h"
#include "ui/wrap/padding_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
namespace Ui {
ChooseTimeResult ChooseTimeWidget(
not_null<RpWidget*> parent,
TimeId startSeconds,
bool hiddenDaysInput) {
using TimeField = Ui::TimePartWithPlaceholder;
const auto putNext = [](not_null<TimeField*> field, QChar ch) {
field->setCursorPosition(0);
if (ch.unicode()) {
field->setText(ch + field->getLastText());
field->setCursorPosition(1);
}
field->onTextEdited();
field->setFocus();
};
const auto erasePrevious = [](not_null<TimeField*> field) {
const auto text = field->getLastText();
if (!text.isEmpty()) {
field->setCursorPosition(text.size() - 1);
field->setText(text.mid(0, text.size() - 1));
}
field->setFocus();
};
struct State {
not_null<TimeField*> day;
not_null<TimeField*> hour;
not_null<TimeField*> minute;
rpl::variable<int> valueInSeconds = 0;
};
auto content = object_ptr<Ui::FixedHeightWidget>(
parent,
st::scheduleHeight);
const auto startDays = startSeconds / 86400;
startSeconds -= startDays * 86400;
const auto startHours = startSeconds / 3600;
startSeconds -= startHours * 3600;
const auto startMinutes = startSeconds / 60;
const auto state = content->lifetime().make_state<State>(State{
.day = Ui::CreateChild<TimeField>(
content.data(),
st::muteBoxTimeField,
rpl::never<QString>(),
QString::number(startDays)),
.hour = Ui::CreateChild<TimeField>(
content.data(),
st::muteBoxTimeField,
rpl::never<QString>(),
QString::number(startHours)),
.minute = Ui::CreateChild<TimeField>(
content.data(),
st::muteBoxTimeField,
rpl::never<QString>(),
QString::number(startMinutes)),
});
const auto day = base::make_weak(state->day);
const auto hour = base::make_weak(state->hour);
const auto minute = base::make_weak(state->minute);
if (hiddenDaysInput) {
day->setVisible(false);
}
day->setPhrase(tr::lng_days);
day->setMaxValue(31);
day->setWheelStep(1);
day->putNext() | rpl::on_next([=](QChar ch) {
putNext(hour.get(), ch);
}, content->lifetime());
hour->setPhrase(tr::lng_hours);
hour->setMaxValue(23);
hour->setWheelStep(1);
hour->putNext() | rpl::on_next([=](QChar ch) {
putNext(minute.get(), ch);
}, content->lifetime());
hour->erasePrevious() | rpl::on_next([=] {
erasePrevious(day.get());
}, content->lifetime());
minute->setPhrase(tr::lng_minutes);
minute->setMaxValue(59);
minute->setWheelStep(10);
minute->erasePrevious() | rpl::on_next([=] {
erasePrevious(hour.get());
}, content->lifetime());
content->sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto inputWidth = s.width() / (hiddenDaysInput ? 2 : 3);
auto rect = QRect(
0,
(s.height() - day->height()) / 2,
inputWidth,
day->height());
for (const auto &input : { day, hour, minute }) {
if (input->isHidden()) {
continue;
}
input->setGeometry(rect - st::muteBoxTimeFieldPadding);
rect.translate(inputWidth, 0);
}
}, content->lifetime());
rpl::merge(
rpl::single(rpl::empty),
base::qt_signal_producer(day.get(), &MaskedInputField::changed),
base::qt_signal_producer(hour.get(), &MaskedInputField::changed),
base::qt_signal_producer(minute.get(), &MaskedInputField::changed)
) | rpl::on_next([=] {
state->valueInSeconds = 0
+ day->getLastText().toUInt() * 3600 * 24
+ hour->getLastText().toUInt() * 3600
+ minute->getLastText().toUInt() * 60;
}, content->lifetime());
return {
object_ptr<Ui::RpWidget>::fromRaw(content.release()),
state->valueInSeconds.value(),
};
}
} // namespace Ui

View File

@@ -0,0 +1,26 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class RpWidget;
struct ChooseTimeResult {
object_ptr<RpWidget> widget;
rpl::producer<TimeId> secondsValue;
};
ChooseTimeResult ChooseTimeWidget(
not_null<RpWidget*> parent,
TimeId startSeconds,
bool hiddenDaysInput = false);
} // namespace Ui

View File

@@ -0,0 +1,277 @@
/*
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 "ui/boxes/collectible_info_box.h"
#include "base/unixtime.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "settings/settings_common.h"
#include "styles/style_boxes.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include <QtCore/QRegularExpression>
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
constexpr auto kTonMultiplier = uint64(1000000000);
[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) {
switch (type) {
case CollectibleType::Phone: {
static const auto kNonDigits = QRegularExpression(u"[^\\d]"_q);
entity.replace(kNonDigits, QString());
} return Ui::FormatPhone(entity);
case CollectibleType::Username:
return entity.startsWith('@') ? entity : ('@' + entity);
}
Unexpected("CollectibleType in FormatEntity.");
}
[[nodiscard]] QString FormatDate(TimeId date) {
return langDateTime(base::unixtime::parse(date));
}
[[nodiscard]] TextWithEntities FormatPrice(const CollectibleInfo &info) {
auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount);
if (minor.size() == 1 && minor.at(0) == '.') {
minor += '0';
}
auto price = (info.cryptoCurrency == u"TON"_q)
? Ui::Text::IconEmoji(
&st::tonIconEmoji
).append(
Info::ChannelEarn::MajorPart(info.cryptoAmount)
).append(minor)
: TextWithEntities{ ('{'
+ info.cryptoCurrency + ':' + QString::number(info.cryptoAmount)
+ '}') };
const auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency);
return Ui::Text::Wrapped(
price,
EntityType::Bold
).append(u" ("_q + fiat + ')');
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeOwnerCell(
not_null<QWidget*> parent,
const CollectibleInfo &info) {
const auto st = &st::defaultMultiSelectItem;
const auto size = st->height;
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), size);
const auto raw = result.data();
const auto name = info.ownerName;
const auto userpic = info.ownerUserpic;
const auto nameWidth = st->style.font->width(name);
const auto added = size + st->padding.left() + st->padding.right();
const auto subscribed = std::make_shared<bool>(false);
raw->paintRequest() | rpl::on_next([=] {
const auto use = std::min(nameWidth + added, raw->width());
const auto x = (raw->width() - use) / 2;
if (const auto available = use - added; available > 0) {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st->textBg);
p.drawRoundedRect(x, 0, use, size, size / 2., size / 2.);
if (!*subscribed) {
*subscribed = true;
userpic->subscribeToUpdates([=] { raw->update(); });
}
p.drawImage(QRect(x, 0, size, size), userpic->image(size));
const auto textx = x + size + st->padding.left();
const auto texty = st->padding.top() + st->style.font->ascent;
const auto text = (use == nameWidth + added)
? name
: st->style.font->elided(name, available);
p.setPen(st->textFg);
p.setFont(st->style.font);
p.drawText(textx, texty, text);
}
}, raw->lifetime());
return result;
}
} // namespace
CollectibleType DetectCollectibleType(const QString &entity) {
return entity.startsWith('+')
? CollectibleType::Phone
: CollectibleType::Username;
}
void CollectibleInfoBox(
not_null<Ui::GenericBox*> box,
CollectibleInfo info) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::collectibleBox);
const auto type = DetectCollectibleType(info.entity);
const auto icon = box->addRow(
object_ptr<Ui::FixedHeightWidget>(box, st::collectibleIconDiameter),
st::collectibleIconPadding);
icon->paintRequest(
) | rpl::on_next([=](QRect clip) {
const auto size = icon->height();
const auto inner = QRect(
(icon->width() - size) / 2,
0,
size,
size);
if (!inner.intersects(clip)) {
return;
}
auto p = QPainter(icon);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::defaultActiveButton.textBg);
p.setPen(Qt::NoPen);
p.drawEllipse(inner);
}, icon->lifetime());
const auto lottieSize = st::collectibleIcon;
auto lottie = Settings::CreateLottieIcon(
icon,
{
.name = (type == CollectibleType::Phone
? u"collectible_phone"_q
: u"collectible_username"_q),
.color = &st::defaultActiveButton.textFg,
.sizeOverride = { lottieSize, lottieSize },
},
QMargins());
box->showFinishes(
) | rpl::on_next([animate = std::move(lottie.animate)] {
animate(anim::repeat::once);
}, box->lifetime());
const auto animation = lottie.widget.release();
icon->sizeValue() | rpl::on_next([=](QSize size) {
const auto skip = (type == CollectibleType::Phone)
? style::ConvertScale(2)
: 0;
animation->move(
(size.width() - animation->width()) / 2,
skip + (size.height() - animation->height()) / 2);
}, animation->lifetime());
const auto formatted = FormatEntity(type, info.entity);
const auto header = (type == CollectibleType::Phone)
? tr::lng_collectible_phone_title(
tr::now,
lt_phone,
tr::link(formatted),
tr::marked)
: tr::lng_collectible_username_title(
tr::now,
lt_username,
tr::link(formatted),
tr::marked);
const auto copyCallback = [box, type, formatted, text = info.copyText](
bool copyLink) {
QGuiApplication::clipboard()->setText((text.isEmpty() || !copyLink)
? formatted
: text);
box->uiShow()->showToast((type == CollectibleType::Phone)
? tr::lng_collectible_phone_copied(tr::now)
: copyLink
? tr::lng_username_copied(tr::now)
: tr::lng_username_text_copied(tr::now));
};
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
rpl::single(header),
st::collectibleHeader),
st::collectibleHeaderPadding,
style::al_top
)->setClickHandlerFilter([copyCallback](const auto &...) {
copyCallback(false);
return false;
});
box->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding);
const auto text = ((type == CollectibleType::Phone)
? tr::lng_collectible_phone_info
: tr::lng_collectible_username_info)(
tr::now,
lt_date,
TextWithEntities{ FormatDate(info.date) },
lt_price,
FormatPrice(info),
tr::rich);
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(box, st::collectibleInfo),
st::collectibleInfoPadding,
style::al_top);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setMarkedText(text);
const auto more = box->addRow(
object_ptr<Ui::RoundButton>(
box,
tr::lng_collectible_learn_more(),
st::collectibleMore),
st::collectibleMorePadding);
more->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
more->setClickedCallback([url = info.url] {
File::OpenUrl(url);
});
const auto phrase = (type == CollectibleType::Phone)
? tr::lng_collectible_phone_copy
: tr::lng_collectible_username_copy;
auto owned = object_ptr<Ui::RoundButton>(
box,
phrase(),
st::collectibleCopy);
const auto copy = owned.data();
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
copy->setClickedCallback([copyCallback] {
copyCallback(true);
});
box->addButton(std::move(owned));
box->setNoContentMargin(true);
const auto buttonsParent = box->verticalLayout().get();
const auto close = Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::boxTitleClose);
close->setClickedCallback([=] {
box->closeBox();
});
box->widthValue(
) | rpl::on_next([=](int width) {
close->moveToRight(0, 0);
}, box->lifetime());
box->widthValue() | rpl::on_next([=](int width) {
more->setFullWidth(width
- st::collectibleMorePadding.left()
- st::collectibleMorePadding.right());
copy->setFullWidth(width
- st::collectibleBox.buttonPadding.left()
- st::collectibleBox.buttonPadding.right());
}, box->lifetime());
}
} // namespace Ui

View File

@@ -0,0 +1,37 @@
/*
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
namespace Ui {
class GenericBox;
class DynamicImage;
enum class CollectibleType {
Phone,
Username,
};
[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity);
struct CollectibleInfo {
QString entity;
QString copyText;
std::shared_ptr<DynamicImage> ownerUserpic;
QString ownerName;
uint64 cryptoAmount = 0;
uint64 amount = 0;
QString cryptoCurrency;
QString currency;
QString url;
TimeId date = 0;
};
void CollectibleInfoBox(not_null<Ui::GenericBox*> box, CollectibleInfo info);
} // namespace Ui

View File

@@ -0,0 +1,156 @@
/*
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 "ui/boxes/confirm_box.h"
#include "lang/lang_keys.h"
#include "ui/rect.h"
#include "ui/widgets/buttons.h"
#include "styles/style_layers.h"
namespace Ui {
void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {
const auto weak = base::make_weak(box);
const auto lifetime = box->lifetime().make_state<rpl::lifetime>();
const auto withTitle = !v::is_null(args.title);
if (withTitle) {
box->setTitle(v::text::take_marked(std::move(args.title)));
}
if (!v::is_null(args.text)) {
const auto padding = st::boxPadding;
const auto use = args.labelPadding
? *args.labelPadding
: withTitle
? QMargins(padding.left(), 0, padding.right(), padding.bottom())
: padding;
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
v::text::take_marked(std::move(args.text)),
args.labelStyle ? *args.labelStyle : st::boxLabel),
use);
if (args.labelFilter) {
label->setClickHandlerFilter(std::move(args.labelFilter));
}
}
const auto prepareCallback = [&](ConfirmBoxArgs::Callback &callback) {
return [=, confirmed = std::move(callback)]() {
if (const auto callbackPtr = std::get_if<1>(&confirmed)) {
if (auto callback = (*callbackPtr)) {
callback();
}
} else if (const auto callbackPtr = std::get_if<2>(&confirmed)) {
if (auto callback = (*callbackPtr)) {
callback(crl::guard(weak, [=] { weak->closeBox(); }));
}
} else if (weak) {
weak->closeBox();
}
};
};
const auto &defaultButtonStyle = box->getDelegate()->style().button;
const auto confirmTextPlain = v::is_null(args.confirmText)
|| v::is<rpl::producer<QString>>(args.confirmText)
|| v::is<QString>(args.confirmText);
const auto confirmButton = box->addButton(
(confirmTextPlain
? v::text::take_plain(
std::move(args.confirmText),
tr::lng_box_ok())
: rpl::single(QString())),
[=, c = prepareCallback(args.confirmed)]() {
lifetime->destroy();
c();
},
args.confirmStyle ? *args.confirmStyle : defaultButtonStyle);
if (!confirmTextPlain) {
confirmButton->setText(
v::text::take_marked(std::move(args.confirmText)));
}
box->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
if ((e->type() != QEvent::KeyPress) || !confirmButton) {
return;
}
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
confirmButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}
}, box->lifetime());
if (!args.inform) {
const auto cancelButton = box->addButton(
v::text::take_plain(std::move(args.cancelText), tr::lng_cancel()),
crl::guard(weak, [=, c = prepareCallback(args.cancelled)]() {
lifetime->destroy();
c();
}),
args.cancelStyle ? *args.cancelStyle : defaultButtonStyle);
box->boxClosing(
) | rpl::on_next(crl::guard(cancelButton, [=] {
cancelButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}), *lifetime);
}
if (args.strictCancel) {
lifetime->destroy();
}
}
object_ptr<Ui::GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args) {
return Box(ConfirmBox, std::move(args));
}
void IconWithTitle(
not_null<VerticalLayout*> container,
not_null<RpWidget*> icon,
not_null<RpWidget*> title,
RpWidget *subtitle) {
const auto line = container->add(
object_ptr<RpWidget>(container),
st::boxRowPadding);
icon->setParent(line);
title->setParent(line);
if (subtitle) {
subtitle->setParent(line);
}
icon->heightValue(
) | rpl::on_next([=](int height) {
line->resize(line->width(), height);
}, icon->lifetime());
line->widthValue(
) | rpl::on_next([=](int width) {
icon->moveToLeft(0, 0);
const auto skip = st::defaultBoxCheckbox.textPosition.x();
title->resizeToWidth(width - rect::right(icon) - skip);
if (subtitle) {
subtitle->resizeToWidth(title->width());
title->moveToLeft(rect::right(icon) + skip, icon->y());
subtitle->moveToLeft(
title->x(),
icon->y() + icon->height() - subtitle->height());
} else {
title->moveToLeft(
rect::right(icon) + skip,
((icon->height() - title->height()) / 2));
}
}, title->lifetime());
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
title->setAttribute(Qt::WA_TransparentForMouseEvents);
}
} // namespace Ui

View File

@@ -0,0 +1,69 @@
/*
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 "ui/layers/generic_box.h"
#include "ui/text/text_variant.h"
namespace Ui {
struct ConfirmBoxArgs {
using Callback = std::variant<
v::null_t,
Fn<void()>,
Fn<void(Fn<void()>)>>;
v::text::data text = v::null;
Callback confirmed = v::null;
Callback cancelled = v::null;
v::text::data confirmText;
v::text::data cancelText;
const style::RoundButton *confirmStyle = nullptr;
const style::RoundButton *cancelStyle = nullptr;
const style::FlatLabel *labelStyle = nullptr;
Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)> labelFilter;
std::optional<QMargins> labelPadding;
v::text::data title = v::null;
bool inform = false;
// If strict cancel is set the cancel.callback() is only called
// if the cancel button was pressed.
bool strictCancel = false;
};
void ConfirmBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args);
inline void InformBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args) {
args.inform = true;
ConfirmBox(box, std::move(args));
}
[[nodiscard]] object_ptr<GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args);
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
ConfirmBoxArgs &&args) {
args.inform = true;
return MakeConfirmBox(std::move(args));
}
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
v::text::data text) {
return MakeInformBox({ .text = std::move(text) });
}
void IconWithTitle(
not_null<VerticalLayout*> container,
not_null<RpWidget*> icon,
not_null<RpWidget*> title,
RpWidget *subtitle = nullptr);
} // namespace Ui

View File

@@ -0,0 +1,192 @@
/*
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 "ui/boxes/confirm_phone_box.h"
#include "core/file_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_utilities.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
namespace Ui {
ConfirmPhoneBox::ConfirmPhoneBox(
QWidget*,
const QString &phone,
int codeLength,
const QString &openUrl,
std::optional<int> timeout)
: _phone(phone)
, _sentCodeLength(codeLength)
, _call([=] { sendCall(); }, [=] { update(); }) {
if (!openUrl.isEmpty()) {
_fragment.create(
this,
tr::lng_intro_fragment_button(),
st::fragmentBoxButton);
_fragment->setClickedCallback([=] { File::OpenUrl(openUrl); });
_fragment->setTextTransform(
Ui::RoundButton::TextTransform::NoTransform);
}
if (timeout) {
_call.setStatus({ Ui::SentCodeCall::State::Waiting, *timeout });
}
}
void ConfirmPhoneBox::sendCall() {
_resendRequests.fire({});
}
void ConfirmPhoneBox::prepare() {
_about.create(
this,
tr::lng_confirm_phone_about(
lt_phone,
rpl::single(tr::bold(Ui::FormatPhone(_phone))),
tr::marked),
st::confirmPhoneAboutLabel);
_code.create(this, st::confirmPhoneCodeField, tr::lng_code_ph());
_code->setAutoSubmit(_sentCodeLength, [=] { sendCode(); });
_code->setChangedCallback([=] { showError(QString()); });
setTitle(tr::lng_confirm_phone_title());
addButton(tr::lng_confirm_phone_send(), [=] { sendCode(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
setDimensions(
st::boxWidth,
st::usernamePadding.top()
+ _code->height()
+ st::usernameSkip
+ _about->height()
+ st::usernameSkip
+ (_fragment ? (_fragment->height() + fragmentSkip()) : 0));
_code->submits(
) | rpl::on_next([=] { sendCode(); }, _code->lifetime());
showChildren();
}
void ConfirmPhoneBox::sendCode() {
if (_isWaitingCheck) {
return;
}
const auto code = _code->getDigitsOnly();
if (code.isEmpty()) {
_code->showError();
return;
}
_code->setDisabled(true);
setFocus();
showError(QString());
_checkRequests.fire_copy(code);
_isWaitingCheck = true;
}
void ConfirmPhoneBox::showError(const QString &error) {
_error = error;
if (!_error.isEmpty()) {
_code->showError();
}
update();
}
void ConfirmPhoneBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
auto p = QPainter(this);
p.setFont(st::boxTextFont);
const auto callText = _call.getText();
if (!callText.isEmpty()) {
p.setPen(st::usernameDefaultFg);
const auto callTextRect = QRect(
st::usernamePadding.left(),
_about->y() + _about->height(),
width() - 2 * st::usernamePadding.left(),
st::usernameSkip);
p.drawText(callTextRect, callText, style::al_left);
}
auto errorText = _error;
if (errorText.isEmpty()) {
p.setPen(st::usernameDefaultFg);
errorText = tr::lng_confirm_phone_enter_code(tr::now);
} else {
p.setPen(st::boxTextFgError);
}
const auto errorTextRect = QRect(
st::usernamePadding.left(),
_code->y() + _code->height(),
width() - 2 * st::usernamePadding.left(),
st::usernameSkip);
p.drawText(errorTextRect, errorText, style::al_left);
}
void ConfirmPhoneBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_code->resize(
width() - st::usernamePadding.left() - st::usernamePadding.right(),
_code->height());
_code->moveToLeft(st::usernamePadding.left(), st::usernamePadding.top());
if (_fragment) {
_fragment->setFullWidth(_code->width());
_fragment->moveToLeft(
(width() - _fragment->width()) / 2,
_code->y() + _code->height() + st::usernameSkip);
}
const auto aboutTop = _fragment
? (_fragment->y() + _fragment->height() + fragmentSkip())
: (_code->y() + _code->height() + st::usernameSkip);
_about->moveToLeft(st::usernamePadding.left(), aboutTop);
}
void ConfirmPhoneBox::setInnerFocus() {
_code->setFocusFast();
}
int ConfirmPhoneBox::fragmentSkip() const {
return st::usernamePadding.bottom();
}
rpl::producer<QString> ConfirmPhoneBox::checkRequests() const {
return _checkRequests.events();
}
rpl::producer<> ConfirmPhoneBox::resendRequests() const {
return _resendRequests.events();
}
void ConfirmPhoneBox::callDone() {
_call.callDone();
}
void ConfirmPhoneBox::showServerError(const QString &text) {
_isWaitingCheck = false;
_code->setDisabled(false);
_code->setFocus();
showError(text);
}
QString ConfirmPhoneBox::getPhone() const {
return _phone;
}
} // namespace Ui

View File

@@ -0,0 +1,71 @@
/*
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 "ui/layers/box_content.h"
#include "ui/widgets/sent_code_field.h"
namespace Ui {
class FlatLabel;
class RoundButton;
class ConfirmPhoneBox final : public Ui::BoxContent {
public:
ConfirmPhoneBox(
QWidget*,
const QString &phone,
int codeLength,
const QString &openUrl,
std::optional<int> timeout);
[[nodiscard]] rpl::producer<QString> checkRequests() const;
[[nodiscard]] rpl::producer<> resendRequests() const;
void callDone();
void showServerError(const QString &text);
protected:
void prepare() override;
void setInnerFocus() override;
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void sendCode();
void sendCall();
void checkPhoneAndHash();
[[nodiscard]] int fragmentSkip() const;
QString getPhone() const;
void showError(const QString &error);
// _hash from the link for account.sendConfirmPhoneCode call.
// _phoneHash from auth.sentCode for account.confirmPhone call.
const QString _phone;
// If we receive the code length, we autosubmit _code field when enough symbols is typed.
const int _sentCodeLength = 0;
bool _isWaitingCheck = false;
object_ptr<Ui::FlatLabel> _about = { nullptr };
object_ptr<Ui::SentCodeField> _code = { nullptr };
object_ptr<Ui::RoundButton> _fragment = { nullptr };
QString _error;
Ui::SentCodeCall _call;
rpl::event_stream<QString> _checkRequests;
rpl::event_stream<> _resendRequests;
};
} // namespace Ui

View File

@@ -0,0 +1,487 @@
/*
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 "ui/boxes/country_select_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/multi_select.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "countries/countries_instance.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_intro.h"
#include <QtCore/QRegularExpression>
namespace Ui {
namespace {
QString LastValidISO;
} // namespace
class CountrySelectBox::Inner : public RpWidget {
public:
Inner(QWidget *parent, const QString &iso, Type type);
~Inner();
void updateFilter(QString filter = QString());
void selectSkip(int32 dir);
void selectSkipPage(int32 h, int32 dir);
void chooseCountry();
void refresh();
[[nodiscard]] rpl::producer<Entry> countryChosen() const {
return _countryChosen.events();
}
[[nodiscard]] rpl::producer<ScrollToRequest> mustScrollTo() const {
return _mustScrollTo.events();
}
protected:
void paintEvent(QPaintEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
void init();
void updateSelected() {
updateSelected(mapFromGlobal(QCursor::pos()));
}
void updateSelected(QPoint localPos);
void updateSelectedRow();
void updateRow(int index);
void setPressed(int pressed);
const std::vector<Entry> &current() const;
Type _type = Type::Phones;
int _rowHeight = 0;
int _selected = -1;
int _pressed = -1;
QString _filter;
bool _mouseSelection = false;
std::vector<std::unique_ptr<RippleAnimation>> _ripples;
std::vector<Entry> _list;
std::vector<Entry> _filtered;
base::flat_map<QChar, std::vector<int>> _byLetter;
std::vector<std::vector<QString>> _namesList;
rpl::event_stream<Entry> _countryChosen;
rpl::event_stream<ScrollToRequest> _mustScrollTo;
};
CountrySelectBox::CountrySelectBox(QWidget*)
: CountrySelectBox(nullptr, QString(), Type::Phones) {
}
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
: _select(this, st::defaultMultiSelect, tr::lng_country_ph())
, _ownedInner(this, iso, type) {
}
rpl::producer<QString> CountrySelectBox::countryChosen() const {
Expects(_ownedInner != nullptr || _inner != nullptr);
return (_ownedInner
? _ownedInner.data()
: _inner.data())->countryChosen() | rpl::map([](const Entry &e) {
return e.iso2;
});
}
rpl::producer<CountrySelectBox::Entry> CountrySelectBox::entryChosen() const {
Expects(_ownedInner != nullptr || _inner != nullptr);
return (_ownedInner
? _ownedInner.data()
: _inner.data())->countryChosen();
}
void CountrySelectBox::prepare() {
setTitle(tr::lng_country_select());
_select->resizeToWidth(st::boxWidth);
_select->setQueryChangedCallback([=](const QString &query) {
applyFilterUpdate(query);
});
_select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
submit();
});
_inner = setInnerWidget(
std::move(_ownedInner),
st::countriesScroll,
_select->height());
addButton(tr::lng_close(), [=] { closeBox(); });
setDimensions(st::boxWidth, st::boxMaxListHeight);
_inner->mustScrollTo(
) | rpl::on_next([=](ScrollToRequest request) {
scrollToY(request.ymin, request.ymax);
}, lifetime());
}
void CountrySelectBox::submit() {
_inner->chooseCountry();
}
void CountrySelectBox::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Down) {
_inner->selectSkip(1);
} else if (e->key() == Qt::Key_Up) {
_inner->selectSkip(-1);
} else if (e->key() == Qt::Key_PageDown) {
_inner->selectSkipPage(height() - _select->height(), 1);
} else if (e->key() == Qt::Key_PageUp) {
_inner->selectSkipPage(height() - _select->height(), -1);
} else {
BoxContent::keyPressEvent(e);
}
}
void CountrySelectBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_select->resizeToWidth(width());
_select->moveToLeft(0, 0);
_inner->resizeToWidth(width());
}
void CountrySelectBox::applyFilterUpdate(const QString &query) {
scrollToY(0);
_inner->updateFilter(query);
}
void CountrySelectBox::setInnerFocus() {
_select->setInnerFocus();
}
CountrySelectBox::Inner::Inner(
QWidget *parent,
const QString &iso,
Type type)
: RpWidget(parent)
, _type(type)
, _rowHeight(st::countryRowHeight) {
setAttribute(Qt::WA_OpaquePaintEvent);
const auto &byISO2 = Countries::Instance().byISO2();
if (byISO2.contains(iso)) {
LastValidISO = iso;
}
rpl::single(
) | rpl::then(
Countries::Instance().updated()
) | rpl::on_next([=] {
_mustScrollTo.fire(ScrollToRequest(0, 0));
_list.clear();
_namesList.clear();
init();
const auto filter = _filter;
_filter = u"a"_q;
updateFilter(filter);
}, lifetime());
}
void CountrySelectBox::Inner::init() {
const auto &byISO2 = Countries::Instance().byISO2();
const auto extractEntries = [&](const Countries::Info &info) {
for (const auto &code : info.codes) {
_list.push_back(Entry{
.country = info.name,
.iso2 = info.iso2,
.code = code.callingCode,
.alternativeName = info.alternativeName,
});
}
};
_list.reserve(byISO2.size());
_namesList.reserve(byISO2.size());
const auto l = byISO2.constFind(LastValidISO);
const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr;
if (lastValid) {
extractEntries(*lastValid);
}
for (const auto &entry : Countries::Instance().list()) {
if (&entry != lastValid) {
extractEntries(entry);
}
}
auto index = 0;
for (const auto &info : _list) {
static const auto RegExp = QRegularExpression("[\\s\\-]");
auto full = info.country
+ ' '
+ (!info.alternativeName.isEmpty()
? info.alternativeName
: QString());
const auto namesList = std::move(full).toLower().split(
RegExp,
Qt::SkipEmptyParts);
auto &names = _namesList.emplace_back();
names.reserve(namesList.size());
for (const auto &name : namesList) {
const auto part = name.trimmed();
if (part.isEmpty()) {
continue;
}
const auto ch = part[0];
auto &byLetter = _byLetter[ch];
if (byLetter.empty() || byLetter.back() != index) {
byLetter.push_back(index);
}
names.push_back(part);
}
++index;
}
}
void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
QRect r(e->rect());
p.setClipRect(r);
const auto &list = current();
if (list.empty()) {
p.fillRect(r, st::boxBg);
p.setFont(st::noContactsFont);
p.setPen(st::noContactsColor);
p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center);
return;
}
const auto l = int(list.size());
if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) {
p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg);
}
int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l);
int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l);
for (int32 i = from; i < to; ++i) {
auto selected = (i == (_pressed >= 0 ? _pressed : _selected));
auto y = st::countriesSkip + i * _rowHeight;
p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg);
if (_ripples.size() > i && _ripples[i]) {
_ripples[i]->paint(p, 0, y, width());
if (_ripples[i]->empty()) {
_ripples[i].reset();
}
}
auto code = QString("+") + list[i].code;
auto codeWidth = st::countryRowCodeFont->width(code);
auto name = list[i].country;
auto nameWidth = st::countryRowNameFont->width(name);
auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width;
if (nameWidth > availWidth) {
name = st::countryRowNameFont->elided(name, availWidth);
nameWidth = st::countryRowNameFont->width(name);
}
p.setFont(st::countryRowNameFont);
p.setPen(st::countryRowNameFg);
p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name);
if (_type == Type::Phones) {
p.setFont(st::countryRowCodeFont);
p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg);
p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code);
}
}
}
void CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {
setMouseTracking(true);
}
void CountrySelectBox::Inner::leaveEventHook(QEvent *e) {
_mouseSelection = false;
setMouseTracking(false);
if (_selected >= 0) {
updateSelectedRow();
_selected = -1;
}
}
void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) {
_mouseSelection = true;
updateSelected(e->pos());
}
void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) {
_mouseSelection = true;
updateSelected(e->pos());
setPressed(_selected);
const auto &list = current();
if (_pressed >= 0 && _pressed < list.size()) {
if (_ripples.size() <= _pressed) {
_ripples.reserve(_pressed + 1);
while (_ripples.size() <= _pressed) {
_ripples.push_back(nullptr);
}
}
if (!_ripples[_pressed]) {
auto mask = RippleAnimation::RectMask(QSize(width(), _rowHeight));
_ripples[_pressed] = std::make_unique<RippleAnimation>(st::countryRipple, std::move(mask), [this, index = _pressed] {
updateRow(index);
});
_ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight));
}
}
}
void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
auto pressed = _pressed;
setPressed(-1);
updateSelectedRow();
if (e->button() == Qt::LeftButton) {
if ((pressed >= 0) && pressed == _selected) {
chooseCountry();
}
}
}
void CountrySelectBox::Inner::updateFilter(QString filter) {
const auto words = TextUtilities::PrepareSearchWords(filter);
filter = words.isEmpty() ? QString() : words.join(' ');
if (_filter == filter) {
return;
}
_filter = filter;
const auto findWord = [&](
const std::vector<QString> &names,
const QString &word) {
for (const auto &name : names) {
if (name.startsWith(word)) {
return true;
}
}
return false;
};
const auto hasAllWords = [&](const std::vector<QString> &names) {
for (const auto &word : words) {
if (!findWord(names, word)) {
return false;
}
}
return true;
};
if (!_filter.isEmpty()) {
_filtered.clear();
for (const auto index : _byLetter[_filter[0].toLower()]) {
if (hasAllWords(_namesList[index])) {
_filtered.push_back(_list[index]);
}
}
}
refresh();
_selected = current().empty() ? -1 : 0;
update();
}
void CountrySelectBox::Inner::selectSkip(int32 dir) {
_mouseSelection = false;
const auto &list = current();
int cur = (_selected >= 0) ? _selected : -1;
cur += dir;
if (cur <= 0) {
_selected = list.empty() ? -1 : 0;
} else if (cur >= list.size()) {
_selected = -1;
} else {
_selected = cur;
}
if (_selected >= 0) {
_mustScrollTo.fire(ScrollToRequest(
st::countriesSkip + _selected * _rowHeight,
st::countriesSkip + (_selected + 1) * _rowHeight));
}
update();
}
void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {
int32 points = h / _rowHeight;
if (!points) return;
selectSkip(points * dir);
}
void CountrySelectBox::Inner::chooseCountry() {
const auto &list = current();
_countryChosen.fire_copy((_selected >= 0 && _selected < list.size())
? list[_selected]
: Entry());
}
void CountrySelectBox::Inner::refresh() {
const auto &list = current();
resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip));
}
void CountrySelectBox::Inner::updateSelected(QPoint localPos) {
if (!_mouseSelection) return;
auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos()));
const auto &list = current();
auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1;
if (_selected != selected) {
updateSelectedRow();
_selected = selected;
updateSelectedRow();
}
}
auto CountrySelectBox::Inner::current() const
-> const std::vector<CountrySelectBox::Entry> & {
return _filter.isEmpty() ? _list : _filtered;
}
void CountrySelectBox::Inner::updateSelectedRow() {
updateRow(_selected);
}
void CountrySelectBox::Inner::updateRow(int index) {
if (index >= 0) {
update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);
}
}
void CountrySelectBox::Inner::setPressed(int pressed) {
if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) {
_ripples[_pressed]->lastStop();
}
_pressed = pressed;
}
CountrySelectBox::Inner::~Inner() = default;
} // namespace Ui

View File

@@ -0,0 +1,59 @@
/*
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 "ui/layers/box_content.h"
namespace Countries {
struct Info;
} // namespace Countries
namespace Ui {
class MultiSelect;
class RippleAnimation;
class CountrySelectBox : public BoxContent {
public:
enum class Type {
Phones,
Countries,
};
struct Entry {
QString country;
QString iso2;
QString code;
QString alternativeName;
};
CountrySelectBox(QWidget*);
CountrySelectBox(QWidget*, const QString &iso, Type type);
[[nodiscard]] rpl::producer<QString> countryChosen() const;
[[nodiscard]] rpl::producer<Entry> entryChosen() const;
protected:
void prepare() override;
void setInnerFocus() override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void submit();
void applyFilterUpdate(const QString &query);
object_ptr<MultiSelect> _select;
class Inner;
object_ptr<Inner> _ownedInner;
QPointer<Inner> _inner;
};
} // namespace Ui

View File

@@ -0,0 +1,218 @@
/*
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 "ui/boxes/edit_birthday_box.h"
#include "base/event_filter.h"
#include "data/data_birthday.h"
#include "lang/lang_keys.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/vertical_drum_picker.h"
#include "ui/ui_utility.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include <QtCore/QDate>
namespace Ui {
class GenericBox;
void EditBirthdayBox(
not_null<Ui::GenericBox*> box,
Data::Birthday current,
Fn<void(Data::Birthday)> save,
EditBirthdayType type) {
box->setWidth(st::boxWideWidth);
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::settingsWorkingHoursPicker));
const auto font = st::boxTextFont;
const auto itemHeight = st::settingsWorkingHoursPickerItemHeight;
const auto picker = [=](
int count,
int startIndex,
Fn<void(QPainter &p, QRectF rect, int index)> paint) {
return Ui::CreateChild<Ui::VerticalDrumPicker>(
content,
Ui::VerticalDrumPicker::DefaultPaintCallback(
font,
itemHeight,
paint),
count,
itemHeight,
startIndex);
};
const auto nowDate = QDate::currentDate();
const auto nowYear = nowDate.year();
const auto nowMonth = nowDate.month();
const auto nowDay = nowDate.day();
const auto now = Data::Birthday(nowDay, nowMonth, nowYear);
const auto max = current.year() ? std::max(now, current) : now;
const auto maxYear = max.year();
const auto minYear = Data::Birthday::kYearMin;
const auto yearsCount = (maxYear - minYear + 2); // Last - not set.
const auto yearsStartIndex = current.year()
? (current.year() - minYear)
: (yearsCount - 1);
const auto yearsPaint = [=](QPainter &p, QRectF rect, int index) {
p.drawText(
rect,
(index < yearsCount - 1
? QString::number(minYear + index)
: QString::fromUtf8("\xe2\x80\x94")),
style::al_center);
};
const auto years = picker(yearsCount, yearsStartIndex, yearsPaint);
struct State {
rpl::variable<Ui::VerticalDrumPicker*> months;
rpl::variable<Ui::VerticalDrumPicker*> days;
};
const auto state = content->lifetime().make_state<State>();
// years->value() is valid only after size is set.
rpl::combine(
content->sizeValue(),
state->months.value(),
state->days.value()
) | rpl::on_next([=](
QSize s,
Ui::VerticalDrumPicker *months,
Ui::VerticalDrumPicker *days) {
const auto half = s.width() / 2;
years->setGeometry(half * 3 / 2, 0, half / 2, s.height());
if (months) {
months->setGeometry(half / 2, 0, half, s.height());
}
if (days) {
days->setGeometry(0, 0, half / 2, s.height());
}
}, content->lifetime());
Ui::SendPendingMoveResizeEvents(years);
years->value() | rpl::on_next([=](int yearsIndex) {
const auto year = (yearsIndex == yearsCount - 1)
? 0
: minYear + yearsIndex;
const auto monthsCount = (year == maxYear)
? max.month()
: 12;
const auto monthsStartIndex = std::clamp(
(state->months.current()
? state->months.current()->index()
: current.month()
? (current.month() - 1)
: (now.month() - 1)),
0,
monthsCount - 1);
const auto monthsPaint = [=](QPainter &p, QRectF rect, int index) {
p.drawText(
rect,
Lang::Month(index + 1)(tr::now),
style::al_center);
};
const auto updated = picker(
monthsCount,
monthsStartIndex,
monthsPaint);
delete state->months.current();
state->months = updated;
state->months.current()->show();
}, years->lifetime());
Ui::SendPendingMoveResizeEvents(state->months.current());
state->months.value() | rpl::map([=](Ui::VerticalDrumPicker *picker) {
return picker ? picker->value() : rpl::single(current.month()
? (current.month() - 1)
: (now.month() - 1));
}) | rpl::flatten_latest() | rpl::on_next([=](int monthIndex) {
const auto month = monthIndex + 1;
const auto yearsIndex = years->index();
const auto year = (yearsIndex == yearsCount - 1)
? 0
: minYear + yearsIndex;
const auto daysCount = (year == maxYear && month == max.month())
? max.day()
: (month == 2)
? ((!year || (!(year % 4) && ((year % 100) || !(year % 400))))
? 29
: 28)
: ((month == 4) || (month == 6) || (month == 9) || (month == 11))
? 30
: 31;
const auto daysStartIndex = std::clamp(
(state->days.current()
? state->days.current()->index()
: current.day()
? (current.day() - 1)
: (now.day() - 1)),
0,
daysCount - 1);
const auto daysPaint = [=](QPainter &p, QRectF rect, int index) {
p.drawText(rect, QString::number(index + 1), style::al_center);
};
const auto updated = picker(
daysCount,
daysStartIndex,
daysPaint);
delete state->days.current();
state->days = updated;
state->days.current()->show();
}, years->lifetime());
content->paintRequest(
) | rpl::on_next([=](const QRect &r) {
auto p = QPainter(content);
p.fillRect(r, Qt::transparent);
const auto lineRect = QRect(
0,
content->height() / 2,
content->width(),
st::defaultInputField.borderActive);
p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);
p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);
}, content->lifetime());
base::install_event_filter(box, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
years->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
auto confirmText = (type == EditBirthdayType::Suggest)
? tr::lng_suggest_birthday_box_confirm()
: tr::lng_settings_save();
box->addButton(std::move(confirmText), [=] {
const auto result = Data::Birthday(
state->days.current()->index() + 1,
state->months.current()->index() + 1,
((years->index() == yearsCount - 1)
? 0
: minYear + years->index()));
box->closeBox();
save(result);
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
if (current && type == EditBirthdayType::Edit) {
box->addLeftButton(tr::lng_settings_birthday_reset(), [=] {
box->closeBox();
save(Data::Birthday());
});
}
}
} // namespace Ui

View File

@@ -0,0 +1,30 @@
/*
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
namespace Data {
class Birthday;
} // namespace Data
namespace Ui {
class GenericBox;
enum class EditBirthdayType {
Edit,
Suggest,
ConfirmSuggestion,
};
void EditBirthdayBox(
not_null<Ui::GenericBox*> box,
Data::Birthday current,
Fn<void(Data::Birthday)> save,
EditBirthdayType type = EditBirthdayType::Edit);
} // namespace Ui

View File

@@ -0,0 +1,89 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/boxes/edit_factcheck_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/fields/input_field.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
void EditFactcheckBox(
not_null<Ui::GenericBox*> box,
TextWithEntities current,
int limit,
Fn<void(TextWithEntities)> save,
Fn<void(not_null<Ui::InputField*>)> initField) {
box->setTitle(tr::lng_factcheck_title());
const auto field = box->addRow(object_ptr<Ui::InputField>(
box,
st::factcheckField,
Ui::InputField::Mode::NoNewlines,
tr::lng_factcheck_placeholder(),
TextWithTags{
current.text,
TextUtilities::ConvertEntitiesToTextTags(current.entities)
}));
AddLengthLimitLabel(field, limit);
initField(field);
enum class State {
Initial,
Changed,
Removed,
};
const auto state = box->lifetime().make_state<rpl::variable<State>>(
State::Initial);
field->changes() | rpl::on_next([=] {
const auto now = field->getLastText().trimmed();
*state = !now.isEmpty()
? State::Changed
: current.empty()
? State::Initial
: State::Removed;
}, field->lifetime());
state->value() | rpl::on_next([=](State state) {
box->clearButtons();
if (state == State::Removed) {
box->addButton(tr::lng_box_remove(), [=] {
box->closeBox();
save({});
}, st::attentionBoxButton);
} else if (state == State::Initial) {
box->addButton(tr::lng_settings_save(), [=] {
if (current.empty()) {
field->showError();
} else {
box->closeBox();
}
});
} else {
box->addButton(tr::lng_settings_save(), [=] {
auto result = field->getTextWithAppliedMarkdown();
if (result.text.size() > limit) {
field->showError();
return;
}
box->closeBox();
save({
result.text,
TextUtilities::ConvertTextTagsToEntities(result.tags)
});
});
}
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}, box->lifetime());
box->setFocusCallback([=] {
field->setFocusFast();
});
}

View File

@@ -0,0 +1,21 @@
/*
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 "ui/layers/generic_box.h"
namespace Ui {
class InputField;
} // namespace Ui
void EditFactcheckBox(
not_null<Ui::GenericBox*> box,
TextWithEntities current,
int limit,
Fn<void(TextWithEntities)> save,
Fn<void(not_null<Ui::InputField*>)> initField);

View File

@@ -0,0 +1,391 @@
/*
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 "ui/boxes/edit_invite_link.h"
#include "base/unixtime.h"
#include "lang/lang_keys.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/layers/generic_box.h"
#include "ui/vertical_list.h"
#include "ui/text/format_values.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/effects/credits_graphics.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Ui {
namespace {
constexpr auto kMaxLimit = std::numeric_limits<int>::max();
constexpr auto kHour = 3600;
constexpr auto kDay = 86400;
constexpr auto kMaxLabelLength = 32;
[[nodiscard]] QString FormatExpireDate(TimeId date) {
if (date > 0) {
return langDateTime(base::unixtime::parse(date));
} else if (-date < kDay) {
return tr::lng_hours(tr::now, lt_count, (-date / kHour));
} else if (-date < 7 * kDay) {
return tr::lng_days(tr::now, lt_count, (-date / kDay));
} else {
return tr::lng_weeks(tr::now, lt_count, (-date / (7 * kDay)));
}
}
} // namespace
void EditInviteLinkBox(
not_null<GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done) {
using namespace rpl::mappers;
const auto link = data.link;
const auto isGroup = data.isGroup;
const auto isPublic = data.isPublic;
const auto subscriptionLocked = data.subscriptionCredits > 0;
box->setTitle(link.isEmpty()
? tr::lng_group_invite_new_title()
: tr::lng_group_invite_edit_title());
const auto container = box->verticalLayout();
const auto addTitle = [&](
not_null<VerticalLayout*> container,
rpl::producer<QString> text) {
container->add(
object_ptr<FlatLabel>(
container,
std::move(text),
st::defaultSubsectionTitle),
(st::defaultSubsectionTitlePadding
+ style::margins(0, st::defaultVerticalListSkip, 0, 0)));
};
const auto addDivider = [&](
not_null<VerticalLayout*> container,
rpl::producer<QString> text,
style::margins margins = style::margins()) {
container->add(
object_ptr<DividerLabel>(
container,
object_ptr<FlatLabel>(
container,
std::move(text),
st::boxDividerLabel),
st::defaultBoxDividerLabelPadding),
margins);
};
const auto now = base::unixtime::now();
const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
const auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);
const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
const auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);
using Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;
struct State {
Buttons expireButtons;
Buttons usageButtons;
int expireValue = 0;
int usageValue = 0;
rpl::variable<bool> requestApproval = false;
rpl::variable<bool> subscription = false;
};
const auto state = box->lifetime().make_state<State>(State{
.expireValue = expire,
.usageValue = usage,
.requestApproval = (data.requestApproval && !isPublic),
.subscription = false,
});
const auto requestApproval = (isPublic || subscriptionLocked)
? nullptr
: container->add(
object_ptr<SettingsButton>(
container,
tr::lng_group_invite_request_approve(),
st::settingsButtonNoIcon),
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
if (requestApproval) {
requestApproval->toggleOn(state->requestApproval.value(), true);
requestApproval->setClickedCallback([=] {
state->requestApproval.force_assign(!requestApproval->toggled());
state->subscription.force_assign(false);
});
addDivider(container, rpl::conditional(
state->requestApproval.value(),
(isGroup
? tr::lng_group_invite_about_approve()
: tr::lng_group_invite_about_approve_channel()),
(isGroup
? tr::lng_group_invite_about_no_approve()
: tr::lng_group_invite_about_no_approve_channel())));
}
auto credits = (Ui::NumberInput*)(nullptr);
if (!isPublic && fillSubscription) {
Ui::AddSkip(container);
const auto &[subscription, input] = fillSubscription();
credits = input.get();
subscription->toggleOn(state->subscription.value(), true);
if (subscriptionLocked) {
input->setText(QString::number(data.subscriptionCredits));
input->setReadOnly(true);
state->subscription.force_assign(true);
state->requestApproval.force_assign(false);
subscription->setToggleLocked(true);
subscription->finishAnimating();
}
subscription->setClickedCallback([=, show = box->uiShow()] {
if (subscriptionLocked) {
show->showToast(
tr::lng_group_invite_subscription_toast(tr::now));
return;
}
state->subscription.force_assign(!subscription->toggled());
state->requestApproval.force_assign(false);
});
}
const auto labelField = container->add(
object_ptr<Ui::InputField>(
container,
st::defaultInputField,
tr::lng_group_invite_label_header(),
data.label),
style::margins(
st::defaultSubsectionTitlePadding.left(),
st::defaultVerticalListSkip,
st::defaultSubsectionTitlePadding.right(),
st::defaultVerticalListSkip * 2));
labelField->setMaxLength(kMaxLabelLength);
addDivider(container, tr::lng_group_invite_label_about());
const auto &saveLabel = link.isEmpty()
? tr::lng_formatting_link_create
: tr::lng_settings_save;
box->addButton(saveLabel(), [=] {
const auto label = labelField->getLastText();
const auto expireDate = (state->expireValue == kMaxLimit)
? 0
: (state->expireValue < 0)
? (base::unixtime::now() - state->expireValue)
: state->expireValue;
const auto usageLimit = (state->usageValue == kMaxLimit)
? 0
: state->usageValue;
done(InviteLinkFields{
.link = link,
.label = label,
.expireDate = expireDate,
.usageLimit = usageLimit,
.subscriptionCredits = credits
? credits->getLastText().toInt()
: 0,
.requestApproval = state->requestApproval.current(),
.isGroup = isGroup,
.isPublic = isPublic,
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
if (subscriptionLocked) {
return;
}
addTitle(container, tr::lng_group_invite_expire_title());
const auto expiresWrap = container->add(
object_ptr<VerticalLayout>(container),
style::margins(0, 0, 0, st::defaultVerticalListSkip));
addDivider(
container,
tr::lng_group_invite_expire_about());
const auto usagesSlide = container->add(
object_ptr<SlideWrap<VerticalLayout>>(
container,
object_ptr<VerticalLayout>(container)));
const auto usagesInner = usagesSlide->entity();
addTitle(usagesInner, tr::lng_group_invite_usage_title());
const auto usagesWrap = usagesInner->add(
object_ptr<VerticalLayout>(usagesInner),
style::margins(0, 0, 0, st::defaultVerticalListSkip));
addDivider(usagesInner, tr::lng_group_invite_usage_about());
static const auto addButton = [](
not_null<VerticalLayout*> container,
const std::shared_ptr<RadiobuttonGroup> &group,
int value,
const QString &text) {
return container->add(
object_ptr<Radiobutton>(
container,
group,
value,
text),
st::inviteLinkLimitMargin);
};
const auto regenerate = [=] {
expireGroup->setValue(state->expireValue);
usageGroup->setValue(state->usageValue);
auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
auto defaults = State();
for (auto i = begin(expires); i != end(expires); ++i) {
if (*i == state->expireValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || (now - *i >= state->expireValue)) {
expires.insert(i, state->expireValue);
break;
}
}
for (auto i = begin(usages); i != end(usages); ++i) {
if (*i == state->usageValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || *i > state->usageValue) {
usages.insert(i, state->usageValue);
break;
}
}
state->expireButtons.clear();
state->usageButtons.clear();
for (const auto limit : expires) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_expire_never(tr::now)
: !limit
? tr::lng_group_invite_expire_custom(tr::now)
: FormatExpireDate(limit);
state->expireButtons.emplace(
limit,
addButton(expiresWrap, expireGroup, limit, text));
}
for (const auto limit : usages) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_usage_any(tr::now)
: !limit
? tr::lng_group_invite_usage_custom(tr::now)
: Lang::FormatCountDecimal(limit);
state->usageButtons.emplace(
limit,
addButton(usagesWrap, usageGroup, limit, text));
}
};
const auto guard = base::make_weak(box);
expireGroup->setChangedCallback([=](int value) {
if (value) {
state->expireValue = value;
return;
}
expireGroup->setValue(state->expireValue);
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
const auto save = [=](TimeId result) {
if (!result) {
return;
}
if (guard) {
state->expireValue = result;
regenerate();
}
box->closeBox();
};
const auto now = base::unixtime::now();
const auto time = (state->expireValue == kMaxLimit)
? (now + kDay)
: (state->expireValue > now)
? state->expireValue
: (state->expireValue < 0)
? (now - state->expireValue)
: (now + kDay);
ChooseDateTimeBox(box, {
.title = tr::lng_group_invite_expire_after(),
.submit = tr::lng_settings_save(),
.done = save,
.time = time,
});
}));
});
usageGroup->setChangedCallback([=](int value) {
if (value) {
state->usageValue = value;
return;
}
usageGroup->setValue(state->usageValue);
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
const auto height = st::boxPadding.bottom()
+ st::defaultInputField.heightMin
+ st::boxPadding.bottom();
box->setTitle(tr::lng_group_invite_expire_after());
const auto wrap = box->addRow(object_ptr<FixedHeightWidget>(
box,
height));
const auto input = CreateChild<NumberInput>(
wrap,
st::defaultInputField,
tr::lng_group_invite_custom_limit(),
(state->usageValue == kMaxLimit
? QString()
: QString::number(state->usageValue)),
200'000);
wrap->widthValue(
) | rpl::on_next([=](int width) {
input->resize(width, input->height());
input->moveToLeft(0, st::boxPadding.bottom());
}, input->lifetime());
box->setFocusCallback([=] {
input->setFocusFast();
});
const auto save = [=] {
const auto value = input->getLastText().toInt();
if (value <= 0) {
input->showError();
return;
}
if (guard) {
state->usageValue = value;
regenerate();
}
box->closeBox();
};
QObject::connect(input, &NumberInput::submitted, save);
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
});
regenerate();
usagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1));
usagesSlide->finishAnimating();
}
void CreateInviteLinkBox(
not_null<GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
bool isGroup,
bool isPublic,
Fn<void(InviteLinkFields)> done) {
EditInviteLinkBox(
box,
std::move(fillSubscription),
InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },
std::move(done));
}
} // namespace Ui

View File

@@ -0,0 +1,45 @@
/*
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
namespace Ui {
class GenericBox;
class NumberInput;
class SettingsButton;
struct InviteLinkFields {
QString link;
QString label;
TimeId expireDate = 0;
int usageLimit = 0;
int subscriptionCredits = 0;
bool requestApproval = false;
bool isGroup = false;
bool isPublic = false;
};
struct InviteLinkSubscriptionToggle final {
not_null<Ui::SettingsButton*> button;
not_null<Ui::NumberInput*> amount;
};
void EditInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done);
void CreateInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
bool isGroup,
bool isPublic,
Fn<void(InviteLinkFields)> done);
} // namespace Ui

View File

@@ -0,0 +1,155 @@
/*
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 "ui/boxes/edit_invite_link_session.h"
#include "core/ui_integration.h" // TextContext
#include "data/components/credits.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/boxes/edit_invite_link.h" // InviteLinkSubscriptionToggle
#include "ui/effects/credits_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_channel_earn.h"
#include "styles/style_chat.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Ui {
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer) {
struct State final {
rpl::variable<float64> usdRate = 0;
};
const auto state = box->lifetime().make_state<State>();
const auto currency = u"USD"_q;
const auto container = box->verticalLayout();
const auto toggle = container->add(
object_ptr<SettingsButton>(
container,
tr::lng_group_invite_subscription(),
st::settingsButtonNoIconLocked),
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
const auto maxCredits = peer->session().appConfig().get<int>(
u"stars_subscription_amount_max"_q,
2500);
const auto &st = st::inviteLinkCreditsField;
const auto skip = st.textMargins.top() / 2;
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
box->setShowFinishedCallback([=] {
wrap->toggleOn(toggle->toggledValue());
wrap->finishAnimating();
});
const auto inputContainer = wrap->entity()->add(
CreateSkipWidget(container, st.heightMin - skip));
const auto input = CreateChild<NumberInput>(
inputContainer,
st,
tr::lng_group_invite_subscription_ph(),
QString(),
std::pow(QString::number(maxCredits).size(), 10));
wrap->toggledValue() | rpl::on_next([=](bool shown) {
if (shown) {
input->setFocus();
}
}, input->lifetime());
const auto icon = CreateSingleStarWidget(
inputContainer,
st.style.font->height);
const auto priceOverlay = Ui::CreateChild<Ui::RpWidget>(inputContainer);
priceOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
inputContainer->sizeValue(
) | rpl::on_next([=](const QSize &size) {
input->resize(
size.width() - rect::m::sum::h(st::boxRowPadding),
st.heightMin);
input->moveToLeft(st::boxRowPadding.left(), -skip);
icon->moveToLeft(
st::boxRowPadding.left(),
input->pos().y() + st.textMargins.top());
priceOverlay->resize(size);
}, input->lifetime());
ToggleChildrenVisibility(inputContainer, true);
QObject::connect(input, &Ui::MaskedInputField::changed, [=] {
const auto amount = input->getLastText().toDouble();
if (amount > maxCredits) {
input->setText(QString::number(maxCredits));
}
priceOverlay->update();
});
priceOverlay->paintRequest(
) | rpl::on_next([=, right = st::boxRowPadding.right()] {
if (state->usdRate.current() <= 0) {
return;
}
const auto amount = input->getLastText().toDouble();
if (amount <= 0) {
return;
}
const auto text = tr::lng_group_invite_subscription_price(
tr::now,
lt_cost,
Ui::FillAmountAndCurrency(
amount * state->usdRate.current(),
currency));
auto p = QPainter(priceOverlay);
p.setFont(st.placeholderFont);
p.setPen(st.placeholderFg);
p.setBrush(Qt::NoBrush);
const auto m = QMargins(0, skip, right, 0);
p.drawText(priceOverlay->rect() - m, text, style::al_right);
}, priceOverlay->lifetime());
state->usdRate = peer->session().credits().rateValue(peer);
auto about = object_ptr<Ui::FlatLabel>(
container,
tr::lng_group_invite_subscription_about(
lt_link,
tr::lng_group_invite_subscription_about_link(
lt_emoji,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
tr::rich
) | rpl::map([](TextWithEntities text) {
return tr::link(
std::move(text),
tr::lng_group_invite_subscription_about_url(tr::now));
}),
tr::rich),
st::boxDividerLabel);
Ui::AddSkip(wrap->entity());
Ui::AddSkip(wrap->entity());
container->add(object_ptr<Ui::DividerLabel>(
container,
std::move(about),
st::defaultBoxDividerLabelPadding));
return { toggle, input };
}
} // namespace Ui

View File

@@ -0,0 +1,23 @@
/*
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
class PeerData;
namespace Ui {
class GenericBox;
class SettingsButton;
struct InviteLinkSubscriptionToggle;
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer);
} // namespace Ui

View File

@@ -0,0 +1,997 @@
/*
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 "ui/boxes/peer_qr_box.h"
#include "core/application.h"
#include "data/data_cloud_themes.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "qr/qr_generate.h"
#include "ui/controls/userpic_button.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/effects/animations.h"
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_giveaway.h"
#include "styles/style_credits.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
#include <QtCore/QMimeData>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
namespace Ui {
namespace {
using Colors = std::vector<QColor>;
[[nodiscard]] QMargins NoPhotoBackgroundMargins() {
return QMargins(
st::profileQrBackgroundMargins.left(),
st::profileQrBackgroundMargins.left(),
st::profileQrBackgroundMargins.right(),
st::profileQrBackgroundMargins.bottom());
}
[[nodiscard]] style::font CreateFont(int size, int scale) {
return style::font(
style::ConvertScale(size, scale),
st::profileQrFont->flags(),
st::profileQrFont->family());
}
[[nodiscard]] QImage TelegramQr(
const Qr::Data &data,
int pixel,
int max,
bool hasWhiteBackground) {
Expects(data.size > 0);
constexpr auto kCenterRatio = 0.175;
if (max > 0 && data.size * pixel > max) {
pixel = std::max(max / data.size, 1);
}
auto qr = Qr::Generate(
data,
pixel * style::DevicePixelRatio(),
hasWhiteBackground ? Qt::transparent : Qt::black,
hasWhiteBackground ? Qt::white : Qt::transparent);
{
auto p = QPainter(&qr);
auto hq = PainterHighQualityEnabler(p);
auto svg = QSvgRenderer(u":/gui/plane_white.svg"_q);
const auto size = qr.rect().size();
const auto centerRect = Rect(size)
- Margins((size.width() - (size.width() * kCenterRatio)) / 2);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
if (hasWhiteBackground) {
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.drawEllipse(centerRect);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
svg.render(&p, centerRect);
} else {
p.drawEllipse(centerRect);
p.setCompositionMode(QPainter::CompositionMode_Clear);
svg.render(&p, centerRect);
}
}
return qr;
}
[[nodiscard]] QMargins RoundedMargins(
const QMargins &backgroundMargins,
int photoSize,
int textMaxHeight) {
return (textMaxHeight
? (backgroundMargins + QMargins(0, photoSize / 2, 0, textMaxHeight))
: photoSize
? backgroundMargins + QMargins(0, photoSize / 2, 0, photoSize / 2)
: Margins(backgroundMargins.left()));
}
void Paint(
QPainter &p,
const style::font &font,
const QString &text,
const Colors &backgroundColors,
const QMargins &backgroundMargins,
const QImage &qrImage,
const QRect &qrRect,
int qrMaxSize,
int qrPixel,
int radius,
int textMaxHeight,
int photoSize,
bool hasWhiteBackground) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(hasWhiteBackground ? Qt::white : Qt::transparent);
const auto roundedRect = qrRect
+ RoundedMargins(backgroundMargins, photoSize, textMaxHeight);
p.drawRoundedRect(roundedRect, radius, radius);
if (!qrImage.isNull() && !backgroundColors.empty()) {
constexpr auto kDuration = crl::time(10000);
const auto angle = (crl::now() % kDuration)
/ float64(kDuration) * 360.0;
const auto gradientRotation = int(angle / 45.) * 45;
const auto gradientRotationAdd = angle - gradientRotation;
const auto textAdditionalWidth = backgroundMargins.left();
auto back = Images::GenerateGradient(
qrRect.size() + QSize(textAdditionalWidth, 0),
backgroundColors,
gradientRotation,
1. - (gradientRotationAdd / 45.));
if (hasWhiteBackground) {
p.drawImage(qrRect, back);
}
const auto coloredSize = QSize(back.width(), textMaxHeight);
auto colored = QImage(
coloredSize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
colored.setDevicePixelRatio(style::DevicePixelRatio());
colored.fill(Qt::transparent);
if (textMaxHeight) {
// '@' + QString(32, 'W');
auto p = QPainter(&colored);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::black);
p.setFont(font);
auto option = QTextOption(style::al_center);
option.setWrapMode(QTextOption::WrapAnywhere);
p.drawText(Rect(coloredSize), text, option);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.drawImage(0, -back.height() + textMaxHeight, back);
}
if (!hasWhiteBackground) {
auto copy = qrImage;
{
auto p = QPainter(&copy);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.drawImage(Rect(copy.size()), back);
}
p.drawImage(qrRect, copy);
} else {
p.drawImage(qrRect, qrImage);
}
if (textMaxHeight) {
p.drawImage(
qrRect.x() - textAdditionalWidth / 2,
rect::bottom(qrRect)
+ ((rect::bottom(roundedRect) - rect::bottom(qrRect))
- textMaxHeight) / 2,
colored);
}
}
}
not_null<Ui::RpWidget*> PrepareQrWidget(
not_null<Ui::VerticalLayout*> container,
not_null<Ui::RpWidget*> topWidget,
rpl::producer<int> fontSizeValue,
rpl::producer<bool> userpicToggled,
rpl::producer<bool> backgroundToggled,
rpl::producer<QString> username,
rpl::producer<QString> links,
rpl::producer<Colors> bgs,
rpl::producer<QString> about) {
const auto divider = container->add(
object_ptr<Ui::BoxContentDivider>(container));
struct State final {
explicit State(Fn<void()> callback) : updating(callback) {
updating.start();
}
Ui::Animations::Basic updating;
style::font font;
QImage qrImage;
Colors backgroundColors;
QString text;
QMargins backgroundMargins;
int textWidth = 0;
int textMaxHeight = 0;
int photoSize = 0;
bool backgroundToggled = false;
};
const auto result = Ui::CreateChild<Ui::RpWidget>(divider);
topWidget->setParent(result);
topWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto state = result->lifetime().make_state<State>(
[=] { result->update(); });
const auto qrMaxSize = st::boxWideWidth
- rect::m::sum::h(st::boxRowPadding)
- rect::m::sum::h(st::profileQrBackgroundMargins);
const auto aboutLabel = Ui::CreateChild<Ui::FlatLabel>(
divider,
st::creditsBoxAboutDivider);
rpl::combine(
std::move(fontSizeValue),
std::move(userpicToggled),
std::move(backgroundToggled),
std::move(username),
std::move(bgs),
std::move(links),
std::move(about),
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged())
) | rpl::on_next([=](
int fontSize,
bool userpicToggled,
bool backgroundToggled,
const QString &username,
const Colors &backgroundColors,
const QString &link,
const QString &about,
const auto &) {
state->font = CreateFont(fontSize, style::Scale());
state->backgroundToggled = backgroundToggled;
state->backgroundMargins = userpicToggled
? st::profileQrBackgroundMargins
: NoPhotoBackgroundMargins();
state->photoSize = userpicToggled
? st::defaultUserpicButton.photoSize
: 0;
state->backgroundColors = backgroundColors;
state->text = username.toUpper();
state->textWidth = state->font->width(state->text);
if (!link.isEmpty()) {
const auto remainder = qrMaxSize % st::introQrPixel;
const auto downTo = remainder
? qrMaxSize - remainder
: qrMaxSize;
state->qrImage = TelegramQr(
Qr::Encode(link.toUtf8(), Qr::Redundancy::Default),
st::introQrPixel,
downTo,
backgroundToggled).scaled(
Size(qrMaxSize * style::DevicePixelRatio()),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
} else {
auto image = QImage(
Size(qrMaxSize * style::DevicePixelRatio()),
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::white);
image.setDevicePixelRatio(style::DevicePixelRatio());
state->qrImage = std::move(image);
}
const auto resultWidth = qrMaxSize
+ rect::m::sum::h(state->backgroundMargins);
{
aboutLabel->setText(about);
aboutLabel->resizeToWidth(resultWidth);
}
const auto textMaxWidth = state->backgroundMargins.left()
+ (state->qrImage.width() / style::DevicePixelRatio());
const auto lines = int(state->textWidth / textMaxWidth) + 1;
state->textMaxHeight = state->textWidth
? (state->font->height * lines)
: 0;
const auto whiteMargins = RoundedMargins(
state->backgroundMargins,
state->photoSize,
state->textMaxHeight);
result->resize(
qrMaxSize + rect::m::sum::h(whiteMargins),
qrMaxSize
+ rect::m::sum::v(whiteMargins) // White.
+ rect::m::sum::v(st::profileQrBackgroundPadding) // Gray.
+ state->photoSize / 2
+ aboutLabel->height());
divider->resize(container->width(), result->height());
result->moveToLeft((container->width() - result->width()) / 2, 0);
topWidget->setVisible(userpicToggled);
topWidget->moveToLeft(0, std::numeric_limits<int>::min());
topWidget->raise();
aboutLabel->raise();
aboutLabel->moveToLeft(
result->x(),
divider->height()
- aboutLabel->height()
- st::defaultBoxDividerLabelPadding.top());
}, container->lifetime());
result->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(result);
const auto size = (state->qrImage.size() / style::DevicePixelRatio());
const auto qrRect = Rect(
(result->width() - size.width()) / 2,
state->backgroundMargins.top() + state->photoSize / 2,
size);
p.translate(
0,
st::profileQrBackgroundPadding.top() + state->photoSize / 2);
Paint(
p,
state->font,
state->text,
state->backgroundColors,
state->backgroundMargins,
state->qrImage,
qrRect,
qrMaxSize,
st::introQrPixel,
st::profileQrBackgroundRadius,
state->textMaxHeight,
state->photoSize,
state->backgroundToggled);
if (!state->photoSize) {
return;
}
const auto photoSize = state->photoSize;
const auto top = Ui::GrabWidget(
topWidget,
QRect(),
Qt::transparent).scaled(
Size(photoSize * style::DevicePixelRatio()),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
p.drawPixmap((result->width() - photoSize) / 2, -photoSize / 2, top);
}, result->lifetime());
return result;
}
[[nodiscard]] Fn<void(int)> AddDotsToSlider(
not_null<Ui::ContinuousSlider*> slider,
const style::MediaSlider &st,
int count) {
const auto lineWidth = st::lineWidth;
const auto smallSize = Size(st.seekSize.height() - st.width);
auto smallDots = std::vector<not_null<Ui::RpWidget*>>();
smallDots.reserve(count - 1);
const auto paintSmall = [=](QPainter &p, const QBrush &brush) {
auto hq = PainterHighQualityEnabler(p);
auto pen = st::boxBg->p;
pen.setWidth(st.width);
p.setPen(pen);
p.setBrush(brush);
p.drawEllipse(Rect(smallSize) - Margins(lineWidth));
};
for (auto i = 0; i < count - 1; i++) {
smallDots.push_back(
Ui::CreateChild<Ui::RpWidget>(slider->parentWidget()));
const auto dot = smallDots.back();
dot->resize(smallSize);
dot->setAttribute(Qt::WA_TransparentForMouseEvents);
dot->paintRequest() | rpl::on_next([=] {
auto p = QPainter(dot);
const auto fg = (slider->value() > (i / float64(count - 1)))
? st.activeFg
: st.inactiveFg;
paintSmall(p, fg);
}, dot->lifetime());
}
const auto bigDot = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());
bigDot->resize(st.seekSize);
bigDot->setAttribute(Qt::WA_TransparentForMouseEvents);
bigDot->paintRequest() | rpl::on_next([=] {
auto p = QPainter(bigDot);
auto hq = PainterHighQualityEnabler(p);
auto pen = st::boxBg->p;
pen.setWidth(st.width);
p.setPen(pen);
p.setBrush(st.activeFg);
p.drawEllipse(Rect(st.seekSize) - Margins(lineWidth));
}, bigDot->lifetime());
return [=](int index) {
const auto g = slider->geometry();
const auto bigTop = g.y() + (g.height() - bigDot->height()) / 2;
const auto smallTop = g.y()
+ (g.height() - smallSize.height()) / 2;
for (auto i = 0; i < count; ++i) {
if (index == i) {
const auto x = ((g.width() - bigDot->width()) * i)
/ float64(count - 1);
bigDot->move(g.x() + std::round(x), bigTop);
} else {
const auto k = (i < index) ? i : i - 1;
const auto w = smallDots[k]->width();
smallDots[k]->move(
g.x() + ((g.width() - w) * i) / (count - 1),
smallTop);
}
}
};
}
} // namespace
void FillPeerQrBox(
not_null<Ui::GenericBox*> box,
PeerData *peer,
std::optional<QString> customLink,
rpl::producer<QString> about) {
const auto window = Core::App().findWindow(box);
const auto controller = window ? window->sessionController() : nullptr;
if (!controller) {
return;
}
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
box->setWidth(st::aboutWidth);
box->setTitle(tr::lng_group_invite_context_qr());
box->verticalLayout()->resizeToWidth(box->width());
struct State {
Ui::RpWidget* saveButton = nullptr;
rpl::variable<bool> saveButtonBusy = false;
rpl::variable<bool> userpicToggled = true;
rpl::variable<bool> backgroundToggled = true;
rpl::variable<Colors> bgs;
Ui::Animations::Simple animation;
rpl::variable<int> chosen = 0;
rpl::variable<int> scaleValue = 0;
rpl::variable<int> fontSizeValue = 28;
};
const auto state = box->lifetime().make_state<State>();
state->userpicToggled = !(customLink || !peer);
const auto usernameValue = [=] {
return (customLink || !peer)
? (rpl::single(QString()) | rpl::type_erased)
: Info::Profile::UsernameValue(peer, true) | rpl::map(
[](const auto &username) { return username.text; });
};
const auto linkValue = [=] {
return customLink
? rpl::single(*customLink)
: peer
? Info::Profile::LinkValue(peer, true) | rpl::map(
[](const auto &link) { return link.text; })
: (rpl::single(QString()) | rpl::type_erased);
};
const auto userpic = Ui::CreateChild<Ui::RpWidget>(box);
const auto userpicSize = st::defaultUserpicButton.photoSize;
userpic->resize(Size(userpicSize));
const auto userpicMedia = Ui::MakeUserpicThumbnail(peer
? peer
: controller->session().user().get());
userpicMedia->subscribeToUpdates(
crl::guard(userpic, [=] { userpic->update(); }));
userpic->paintRequest() | rpl::on_next([=] {
auto p = QPainter(userpic);
p.drawImage(0, 0, userpicMedia->image(userpicSize));
}, userpic->lifetime());
linkValue() | rpl::on_next([=](const QString &link) {
if (link.isEmpty()) {
box->showFinishes() | rpl::on_next([=] {
box->closeBox();
}, box->lifetime());
box->closeBox();
}
}, box->lifetime());
userpic->setVisible(peer != nullptr);
PrepareQrWidget(
box->verticalLayout(),
userpic,
state->fontSizeValue.value(),
state->userpicToggled.value(),
state->backgroundToggled.value(),
usernameValue(),
linkValue(),
state->bgs.value(),
about ? std::move(about) : rpl::single(QString()));
Ui::AddSkip(box->verticalLayout());
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_userpic_builder_color_subtitle());
const auto themesContainer = box->addRow(
object_ptr<Ui::VerticalLayout>(box));
const auto activewidth = int(
(st::defaultInputField.borderActive + st::lineWidth) * 0.9);
const auto size = st::chatThemePreviewSize.width();
const auto fill = [=](const std::vector<Data::CloudTheme> &cloudThemes) {
while (themesContainer->count()) {
delete themesContainer->widgetAt(0);
}
struct State {
Colors colors;
QImage image;
};
constexpr auto kMaxInRow = 4;
constexpr auto kMaxColors = 4;
auto row = (Ui::RpWidget*)(nullptr);
auto counter = 0;
const auto spacing = (0
+ (box->width() - rect::m::sum::h(st::boxRowPadding))
- (kMaxInRow * size)) / (kMaxInRow + 1);
auto colorsCollection = ranges::views::all(
cloudThemes
) | ranges::views::transform([](const auto &cloudTheme) -> Colors {
const auto it = cloudTheme.settings.find(
Data::CloudThemeType::Light);
if (it == end(cloudTheme.settings)) {
return Colors();
}
const auto colors = it->second.paper
? it->second.paper->backgroundColors()
: Colors();
if (colors.size() != kMaxColors) {
return Colors();
}
return colors;
}) | ranges::views::filter([](const Colors &colors) {
return !colors.empty();
}) | ranges::to_vector;
Expects(!colorsCollection.empty());
colorsCollection[0] = Colors{
st::premiumButtonBg1->c,
st::premiumButtonBg1->c,
st::premiumButtonBg2->c,
st::premiumButtonBg3->c,
};
// colorsCollection.push_back(Colors{
// st::creditsBg1->c,
// st::creditsBg2->c,
// st::creditsBg1->c,
// st::creditsBg2->c,
// });
for (const auto &colors : colorsCollection) {
if (state->bgs.current().empty()) {
state->bgs = colors;
}
if (counter % kMaxInRow == 0) {
Ui::AddSkip(themesContainer);
row = themesContainer->add(
object_ptr<Ui::RpWidget>(themesContainer));
row->resize(size, size);
}
const auto widget = Ui::CreateChild<Ui::AbstractButton>(row);
widget->setClickedCallback([=] {
state->chosen = counter;
widget->update();
state->animation.stop();
state->animation.start([=](float64 value) {
const auto was = state->bgs.current();
const auto &now = colors;
if (was.size() == now.size()
&& was.size() == kMaxColors) {
state->bgs = Colors({
anim::color(was[0], now[0], value),
anim::color(was[1], now[1], value),
anim::color(was[2], now[2], value),
anim::color(was[3], now[3], value),
});
}
},
0.,
1.,
st::shakeDuration);
});
state->chosen.value() | rpl::combine_previous(
) | rpl::filter([=](int i, int k) {
return i == counter || k == counter;
}) | rpl::on_next([=] {
widget->update();
}, widget->lifetime());
widget->resize(size, size);
widget->moveToLeft(
spacing + ((counter % kMaxInRow) * (size + spacing)),
0);
widget->show();
const auto cornersMask = Images::CornersMask(
st::roundRadiusLarge * style::DevicePixelRatio());
const auto back = [&] {
auto gradient = Images::GenerateGradient(
Size(size - activewidth * 5) * style::DevicePixelRatio(),
colors,
0,
0);
gradient.setDevicePixelRatio(style::DevicePixelRatio());
auto result = Images::Round(std::move(gradient), cornersMask);
const auto rect = Rect(
result.size() / style::DevicePixelRatio());
auto colored = result;
colored.fill(Qt::transparent);
{
auto p = QPainter(&colored);
auto hq = PainterHighQualityEnabler(p);
st::profileQrIcon.paintInCenter(p, rect);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.drawImage(0, 0, result);
}
auto temp = result;
temp.fill(Qt::transparent);
{
auto p = QPainter(&temp);
auto hq = PainterHighQualityEnabler(p);
p.setPen(st::premiumButtonFg);
p.setBrush(st::premiumButtonFg);
const auto size = st::profileQrIcon.width() * 1.5;
const auto margins = Margins((rect.width() - size) / 2);
const auto inner = rect - margins;
p.drawRoundedRect(
inner,
st::roundRadiusLarge,
st::roundRadiusLarge);
p.drawImage(0, 0, colored);
}
{
auto p = QPainter(&result);
p.drawImage(0, 0, temp);
}
return result;
}();
widget->paintRequest() | rpl::on_next([=] {
auto p = QPainter(widget);
const auto rect = widget->rect() - Margins(activewidth * 2.5);
p.drawImage(rect.x(), rect.y(), back);
if (state->chosen.current() == counter) {
auto hq = PainterHighQualityEnabler(p);
auto pen = st::activeLineFg->p;
pen.setWidth(st::defaultInputField.borderActive);
p.setPen(pen);
const auto r = st::roundRadiusLarge
+ activewidth * 2.1 * style::DevicePixelRatio();
p.drawRoundedRect(
widget->rect() - Margins(pen.width()),
r,
r);
}
}, widget->lifetime());
counter++;
}
Ui::AddSkip(themesContainer);
Ui::AddSkip(themesContainer);
themesContainer->resizeToWidth(box->width());
};
const auto themes = &controller->session().data().cloudThemes();
const auto &list = themes->chatThemes();
if (!list.empty()) {
fill(list);
} else {
themes->refreshChatThemes();
themes->chatThemesUpdated(
) | rpl::take(1) | rpl::on_next([=] {
fill(themes->chatThemes());
}, box->lifetime());
}
Ui::AddSkip(box->verticalLayout());
Ui::AddDivider(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_qr_box_quality());
Ui::AddSkip(box->verticalLayout());
constexpr auto kMaxQualities = 3;
{
const auto seekSize = st::settingsScale.seekSize.height();
const auto &labelSt = st::defaultFlatLabel;
const auto labels = box->verticalLayout()->add(
Ui::CreateSkipWidget(
box,
labelSt.style.font->height + labelSt.style.font->descent),
st::boxRowPadding);
const auto left = Ui::CreateChild<Ui::FlatLabel>(
labels,
tr::lng_qr_box_quality1(),
labelSt);
const auto middle = Ui::CreateChild<Ui::FlatLabel>(
labels,
tr::lng_qr_box_quality2(),
labelSt);
const auto right = Ui::CreateChild<Ui::FlatLabel>(
labels,
tr::lng_qr_box_quality3(),
labelSt);
labels->sizeValue(
) | rpl::on_next([=](const QSize &size) {
left->moveToLeft(0, 0);
middle->moveToLeft((size.width() - middle->width()) / 2, 0);
right->moveToRight(0, 0);
}, labels->lifetime());
const auto slider = box->verticalLayout()->add(
object_ptr<Ui::MediaSliderWheelless>(
box->verticalLayout(),
st::settingsScale),
st::boxRowPadding);
slider->resize(slider->width(), seekSize);
const auto active = st::windowActiveTextFg->c;
const auto inactive = st::windowSubTextFg->c;
const auto colorize = [=](int index) {
if (index == 0) {
left->setTextColorOverride(active);
middle->setTextColorOverride(inactive);
right->setTextColorOverride(inactive);
} else if (index == 1) {
left->setTextColorOverride(inactive);
middle->setTextColorOverride(active);
right->setTextColorOverride(inactive);
} else if (index == 2) {
left->setTextColorOverride(inactive);
middle->setTextColorOverride(inactive);
right->setTextColorOverride(active);
}
};
const auto updateGeometry = AddDotsToSlider(
slider,
st::settingsScale,
kMaxQualities);
slider->geometryValue(
) | rpl::on_next([=](const QRect &rect) {
updateGeometry(int(slider->value() * (kMaxQualities - 1)));
}, box->lifetime());
box->setShowFinishedCallback([=] {
colorize(0);
updateGeometry(0);
});
slider->setPseudoDiscrete(
kMaxQualities,
[=](int index) { return index; },
0,
[=](int scale) {
state->scaleValue = scale;
colorize(scale);
updateGeometry(scale);
},
[](int) {});
}
{
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_qr_box_font_size());
Ui::AddSkip(box->verticalLayout());
const auto seekSize = st::settingsScale.seekSize.height();
const auto slider = box->verticalLayout()->add(
object_ptr<Ui::MediaSliderWheelless>(
box->verticalLayout(),
st::settingsScale),
st::boxRowPadding);
slider->resize(slider->width(), seekSize);
const auto kSizeAmount = 8;
const auto kMinSize = 20;
const auto kMaxSize = 36;
const auto kStep = (kMaxSize - kMinSize) / (kSizeAmount - 1);
const auto updateGeometry = AddDotsToSlider(
slider,
st::settingsScale,
kSizeAmount);
const auto fontSizeToIndex = [=](int fontSize) {
return (fontSize - kMinSize) / kStep;
};
const auto indexToFontSize = [=](int index) {
return kMinSize + index * kStep;
};
slider->geometryValue(
) | rpl::on_next([=](const QRect &rect) {
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
}, box->lifetime());
box->setShowFinishedCallback([=] {
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
});
slider->setPseudoDiscrete(
kSizeAmount,
[=](int index) { return indexToFontSize(index); },
state->fontSizeValue.current(),
[=](int fontSize) {
state->fontSizeValue = fontSize;
updateGeometry(fontSizeToIndex(fontSize));
},
[](int) {});
}
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
if (peer) {
const auto userpicToggle = box->verticalLayout()->add(
object_ptr<Ui::SettingsButton>(
box->verticalLayout(),
(peer->isUser()
? tr::lng_mediaview_profile_photo
: (peer->isChannel() && !peer->isMegagroup())
? tr::lng_mediaview_channel_photo
: tr::lng_mediaview_group_photo)(),
st::settingsButtonNoIcon));
userpicToggle->toggleOn(state->userpicToggled.value(), true);
userpicToggle->setClickedCallback([=] {
state->userpicToggled = !state->userpicToggled.current();
});
}
{
const auto backgroundToggle = box->verticalLayout()->add(
object_ptr<Ui::SettingsButton>(
box->verticalLayout(),
tr::lng_qr_box_transparent_background(),
st::settingsButtonNoIcon));
backgroundToggle->toggleOn(
state->backgroundToggled.value() | rpl::map(!rpl::mappers::_1),
true);
backgroundToggle->setClickedCallback([=] {
state->backgroundToggled = !state->backgroundToggled.current();
});
}
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
auto buttonText = rpl::conditional(
state->saveButtonBusy.value() | rpl::map(rpl::mappers::_1),
rpl::single(QString()),
tr::lng_chat_link_copy());
const auto show = controller->uiShow();
state->saveButton = box->addButton(std::move(buttonText), [=] {
if (state->saveButtonBusy.current()) {
return;
}
state->saveButtonBusy = true;
const auto userpicToggled = state->userpicToggled.current();
const auto backgroundToggled = state->backgroundToggled.current();
const auto scale = style::kScaleDefault
* (kMaxQualities + int(state->scaleValue.current() * 2));
const auto divider = std::max(100, style::Scale())
/ style::kScaleDefault;
const auto profileQrBackgroundRadius = style::ConvertScale(
st::profileQrBackgroundRadius / divider,
scale);
const auto introQrPixel = style::ConvertScale(
st::introQrPixel / divider,
scale);
const auto lineWidth = style::ConvertScale(
st::lineWidth / divider,
scale);
const auto boxWideWidth = style::ConvertScale(
st::boxWideWidth / divider,
scale);
const auto createMargins = [&](const style::margins &margins) {
return QMargins(
style::ConvertScale(margins.left() / divider, scale),
style::ConvertScale(margins.top() / divider, scale),
style::ConvertScale(margins.right() / divider, scale),
style::ConvertScale(margins.bottom() / divider, scale));
};
const auto boxRowPadding = createMargins(st::boxRowPadding);
const auto backgroundMargins = userpicToggled
? createMargins(st::profileQrBackgroundMargins)
: createMargins(NoPhotoBackgroundMargins());
const auto qrMaxSize = boxWideWidth
- rect::m::sum::h(boxRowPadding)
- rect::m::sum::h(backgroundMargins);
const auto photoSize = userpicToggled
? style::ConvertScale(
st::defaultUserpicButton.photoSize / divider,
scale)
: 0;
const auto font = CreateFont(state->fontSizeValue.current(), scale);
const auto username = rpl::variable<QString>(
usernameValue()).current().toUpper();
const auto link = rpl::variable<QString>(linkValue());
const auto textWidth = font->width(username);
const auto top = photoSize
? userpicMedia->image(photoSize)
: QImage();
const auto weak = base::make_weak(box);
crl::async([=] {
const auto qrImage = TelegramQr(
Qr::Encode(
link.current().toUtf8(),
Qr::Redundancy::Default),
introQrPixel,
qrMaxSize,
backgroundToggled);
const auto textMaxWidth = backgroundMargins.left()
+ (qrImage.width() / style::DevicePixelRatio());
const auto lines = int(textWidth / textMaxWidth) + 1;
const auto textMaxHeight = textWidth ? font->height * lines : 0;
const auto whiteMargins = RoundedMargins(
backgroundMargins,
photoSize,
textMaxHeight);
const auto resultSize = QSize(
qrMaxSize + rect::m::sum::h(whiteMargins),
qrMaxSize + rect::m::sum::v(whiteMargins) + photoSize / 2);
const auto qrImageSize = qrImage.size()
/ style::DevicePixelRatio();
const auto qrRect = Rect(
(resultSize.width() - qrImageSize.width()) / 2,
whiteMargins.top() + photoSize / 2,
qrImageSize);
auto image = QImage(
resultSize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
image.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&image);
p.translate(0, lineWidth); // Bad.
Paint(
p,
font,
username,
state->bgs.current(),
backgroundMargins,
qrImage,
qrRect,
qrMaxSize,
introQrPixel,
profileQrBackgroundRadius,
textMaxHeight,
photoSize,
backgroundToggled);
if (userpicToggled) {
p.drawImage((resultSize.width() - photoSize) / 2, 0, top);
}
}
crl::on_main(weak, [=] {
state->saveButtonBusy = false;
auto mime = std::make_unique<QMimeData>();
mime->setImageData(std::move(image));
QGuiApplication::clipboard()->setMimeData(mime.release());
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
});
});
});
if (const auto saveButton = state->saveButton) {
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
saveButton,
saveButton->height() / 2);
AddChildToWidgetCenter(saveButton, loadingAnimation);
loadingAnimation->showOn(state->saveButtonBusy.value());
}
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
}
void DefaultShowFillPeerQrBoxCallback(
std::shared_ptr<Ui::Show> show,
PeerData *peer) {
if (peer && !peer->username().isEmpty()) {
show->show(Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));
}
}
} // namespace Ui

View File

@@ -0,0 +1,27 @@
/*
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
class PeerData;
namespace Ui {
class GenericBox;
class Show;
void DefaultShowFillPeerQrBoxCallback(
std::shared_ptr<Ui::Show> show,
PeerData *peer);
void FillPeerQrBox(
not_null<Ui::GenericBox*> box,
PeerData *peer,
std::optional<QString> customLink,
rpl::producer<QString> about);
} // namespace Ui

View File

@@ -0,0 +1,149 @@
/*
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 "ui/boxes/rate_call_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "styles/style_layers.h"
#include "styles/style_calls.h"
namespace Ui {
namespace {
constexpr auto kMaxRating = 5;
constexpr auto kRateCallCommentLengthMax = 200;
} // namespace
RateCallBox::RateCallBox(QWidget*, InputSubmitSettings sendWay)
: _sendWay(sendWay) {
}
void RateCallBox::prepare() {
setTitle(tr::lng_call_rate_label());
addButton(tr::lng_cancel(), [=] { closeBox(); });
for (auto i = 0; i < kMaxRating; ++i) {
_stars.emplace_back(this, st::callRatingStar);
_stars.back()->setClickedCallback([this, value = i + 1] {
ratingChanged(value);
});
_stars.back()->show();
}
updateMaxHeight();
}
void RateCallBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
const auto starsWidth = (_stars.size() * st::callRatingStar.width);
auto starLeft = (width() - starsWidth) / 2;
const auto starTop = st::callRatingStarTop;
for (auto &star : _stars) {
star->moveToLeft(starLeft, starTop);
starLeft += star->width();
}
if (_comment) {
_comment->moveToLeft(
st::callRatingPadding.left(),
_stars.back()->bottomNoMargins() + st::callRatingCommentTop);
}
}
void RateCallBox::ratingChanged(int value) {
Expects(value > 0 && value <= kMaxRating);
if (!_rating) {
clearButtons();
addButton(tr::lng_send_button(), [=] { send(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
}
_rating = value;
for (auto i = 0; i < kMaxRating; ++i) {
_stars[i]->setIconOverride((i < value)
? &st::callRatingStarFilled
: nullptr);
_stars[i]->setRippleColorOverride((i < value)
? &st::lightButtonBgOver
: nullptr);
}
if (value < kMaxRating) {
if (!_comment) {
_comment.create(
this,
st::callRatingComment,
Ui::InputField::Mode::MultiLine,
tr::lng_call_rate_comment());
_comment->show();
_comment->setSubmitSettings(_sendWay);
_comment->setMaxLength(kRateCallCommentLengthMax);
_comment->resize(
width()
- st::callRatingPadding.left()
- st::callRatingPadding.right(),
_comment->height());
updateMaxHeight();
_comment->heightChanges(
) | rpl::on_next([=] {
commentResized();
}, _comment->lifetime());
_comment->submits(
) | rpl::on_next([=] { send(); }, _comment->lifetime());
_comment->cancelled(
) | rpl::on_next([=] {
closeBox();
}, _comment->lifetime());
}
_comment->setFocusFast();
} else if (_comment) {
_comment.destroy();
updateMaxHeight();
}
}
void RateCallBox::setInnerFocus() {
if (_comment) {
_comment->setFocusFast();
} else {
BoxContent::setInnerFocus();
}
}
void RateCallBox::commentResized() {
updateMaxHeight();
update();
}
void RateCallBox::send() {
Expects(_rating > 0 && _rating <= kMaxRating);
_sends.fire({
.rating = _rating,
.comment = _comment ? _comment->getLastText().trimmed() : QString(),
});
}
void RateCallBox::updateMaxHeight() {
auto newHeight = st::callRatingPadding.top()
+ st::callRatingStarTop
+ _stars.back()->heightNoMargins()
+ st::callRatingPadding.bottom();
if (_comment) {
newHeight += st::callRatingCommentTop + _comment->height();
}
setDimensions(st::boxWideWidth, newHeight);
}
rpl::producer<RateCallBox::Result> RateCallBox::sends() const {
return _sends.events();
}
} // namespace Ui

View File

@@ -0,0 +1,51 @@
/*
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 "ui/layers/box_content.h"
namespace Ui {
class InputField;
class IconButton;
enum class InputSubmitSettings;
class RateCallBox : public Ui::BoxContent {
public:
RateCallBox(QWidget*, InputSubmitSettings sendWay);
struct Result {
int rating = 0;
QString comment;
};
[[nodiscard]] rpl::producer<Result> sends() const;
protected:
void prepare() override;
void setInnerFocus() override;
void resizeEvent(QResizeEvent *e) override;
private:
void updateMaxHeight();
void ratingChanged(int value);
void send();
void commentResized();
const InputSubmitSettings _sendWay;
int _rating = 0;
std::vector<object_ptr<Ui::IconButton>> _stars;
object_ptr<Ui::InputField> _comment = { nullptr };
rpl::event_stream<Result> _sends;
};
} // namespace Ui

View File

@@ -0,0 +1,223 @@
/*
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 "ui/boxes/report_box_graphics.h"
#include "info/profile/info_profile_icon.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/settings_common.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
#include "styles/style_channel_earn.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
constexpr auto kReportReasonLengthMax = 512;
using Source = ReportSource;
using Reason = ReportReason;
} // namespace
void ReportReasonBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
ReportSource source,
Fn<void(Reason)> done) {
box->setTitle([&] {
switch (source) {
case Source::Message: return tr::lng_report_message_title();
case Source::Channel: return tr::lng_report_title();
case Source::Group: return tr::lng_report_group_title();
case Source::Bot: return tr::lng_report_bot_title();
case Source::ProfilePhoto:
return tr::lng_report_profile_photo_title();
case Source::ProfileVideo:
return tr::lng_report_profile_video_title();
case Source::GroupPhoto: return tr::lng_report_group_photo_title();
case Source::GroupVideo: return tr::lng_report_group_video_title();
case Source::ChannelPhoto:
return tr::lng_report_channel_photo_title();
case Source::ChannelVideo:
return tr::lng_report_channel_video_title();
case Source::Story:
return tr::lng_report_story();
}
Unexpected("'source' in ReportReasonBox.");
}());
auto margin = style::margins{ 0, st::reportReasonTopSkip, 0, 0 };
const auto add = [&](
Reason reason,
tr::phrase<> text,
const style::icon &icon) {
const auto layout = box->verticalLayout();
const auto button = layout->add(
object_ptr<Ui::SettingsButton>(layout.get(), text(), st.button),
margin);
margin = {};
button->setClickedCallback([=] {
done(reason);
});
const auto height = st.button.padding.top()
+ st.button.height
+ st.button.padding.bottom();
object_ptr<Info::Profile::FloatingIcon>(
button,
icon,
QPoint{
st::infoSharedMediaButtonIconPosition.x(),
(height - icon.height()) / 2,
});
};
add(Reason::Spam, tr::lng_report_reason_spam, st.spam);
if (source == Source::Channel
|| source == Source::Group
|| source == Source::Bot) {
add(Reason::Fake, tr::lng_report_reason_fake, st.fake);
}
add(
Reason::Violence,
tr::lng_report_reason_violence,
st.violence);
add(
Reason::ChildAbuse,
tr::lng_report_reason_child_abuse,
st.children);
add(
Reason::Pornography,
tr::lng_report_reason_pornography,
st.pornography);
add(
Reason::Copyright,
tr::lng_report_reason_copyright,
st.copyright);
if (source == Source::Message || source == Source::Story) {
add(
Reason::IllegalDrugs,
tr::lng_report_reason_illegal_drugs,
st.drugs);
add(
Reason::PersonalDetails,
tr::lng_report_reason_personal_details,
st.personal);
}
add(Reason::Other, tr::lng_report_reason_other, st.other);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void ReportDetailsBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
Fn<void(QString)> done) {
box->setTitle(tr::lng_profile_report());
AddReportDetailsIconButton(box);
Ui::AddSkip(
box->verticalLayout(),
st::settingsBlockedListIconPadding.bottom());
box->addRow(
object_ptr<FlatLabel>(
box, // #TODO reports
tr::lng_report_details_about(),
st.label),
{
st::boxRowPadding.left(),
st::boxPadding.top(),
st::boxRowPadding.right(),
st::boxPadding.bottom(),
});
const auto details = box->addRow(
object_ptr<InputField>(
box,
st.field,
InputField::Mode::MultiLine,
tr::lng_report_details(),
QString()));
details->setMaxLength(kReportReasonLengthMax);
box->setFocusCallback([=] {
details->setFocusFast();
});
const auto submit = [=] {
const auto text = details->getLastText();
done(text);
};
details->submits() | rpl::on_next(submit, details->lifetime());
box->addButton(tr::lng_report_button(), submit);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
not_null<Ui::AbstractButton*> AddReportOptionButton(
not_null<Ui::VerticalLayout*> container,
const QString &text,
const style::ReportBox *stOverride) {
const auto button = container->add(
object_ptr<Ui::SettingsButton>(
container,
rpl::single(QString()),
(stOverride ? stOverride : &st::defaultReportBox)->noIconButton));
const auto textFg = (stOverride
? stOverride->label
: st::sponsoredReportLabel).textFg->c;
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(text),
st::sponsoredReportLabel);
label->setTextColorOverride(textFg);
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
icon->resize(st::settingsPremiumArrow.size());
icon->paintRequest() | rpl::on_next([=, w = icon->width()] {
auto p = Painter(icon);
st::settingsPremiumArrow.paint(p, 0, 0, w, textFg);
}, icon->lifetime());
button->sizeValue() | rpl::on_next([=](const QSize &size) {
const auto left = button->st().padding.left();
const auto right = button->st().padding.right();
icon->moveToRight(right, (size.height() - icon->height()) / 2);
label->resizeToWidth(size.width()
- icon->width()
- left
- st::settingsButtonRightSkip
- right);
label->moveToLeft(left, (size.height() - label->height()) / 2);
button->resize(
button->width(),
rect::m::sum::v(button->st().padding) + label->height());
}, button->lifetime());
label->setAttribute(Qt::WA_TransparentForMouseEvents);
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
return button;
}
void AddReportDetailsIconButton(not_null<GenericBox*> box) {
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = u"blocked_peers_empty"_q,
.sizeOverride = st::normalBoxLottieSize,
},
{});
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
});
box->addRow(std::move(icon.widget));
}
} // namespace Ui

View File

@@ -0,0 +1,64 @@
/*
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
namespace style {
struct ReportBox;
} // namespace style
namespace Ui {
class AbstractButton;
class GenericBox;
class VerticalLayout;
enum class ReportSource {
Message,
Channel,
Group,
Bot,
ProfilePhoto,
ProfileVideo,
GroupPhoto,
GroupVideo,
ChannelPhoto,
ChannelVideo,
Story,
};
enum class ReportReason {
Spam,
Fake,
Violence,
ChildAbuse,
Pornography,
Copyright,
IllegalDrugs,
PersonalDetails,
Other,
};
void ReportReasonBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
ReportSource source,
Fn<void(ReportReason)> done);
void ReportDetailsBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
Fn<void(QString)> done);
[[nodiscard]] not_null<Ui::AbstractButton*> AddReportOptionButton(
not_null<Ui::VerticalLayout*> container,
const QString &text,
const style::ReportBox *stOverride);
void AddReportDetailsIconButton(not_null<GenericBox*> box);
} // namespace Ui

View File

@@ -0,0 +1,219 @@
/*
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 "ui/boxes/show_or_premium_box.h"
#include "base/object_ptr.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/settings_common.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/gradient_round_button.h"
#include "ui/widgets/labels.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
#include "styles/style_boxes.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
constexpr auto kShowOrLineOpacity = 0.3;
} // namespace
object_ptr<RpWidget> MakeShowOrLabel(
not_null<RpWidget*> parent,
rpl::producer<QString> text) {
auto result = object_ptr<FlatLabel>(
parent,
std::move(text),
st::showOrLabel);
const auto raw = result.data();
raw->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(raw);
const auto full = st::showOrLineWidth;
const auto left = (raw->width() - full) / 2;
const auto text = raw->naturalWidth() + 2 * st::showOrLabelSkip;
const auto fill = (full - text) / 2;
const auto stroke = st::lineWidth;
const auto top = st::showOrLineTop;
p.setOpacity(kShowOrLineOpacity);
p.fillRect(left, top, fill, stroke, st::windowSubTextFg);
const auto start = left + full - fill;
p.fillRect(start, top, fill, stroke, st::windowSubTextFg);
}, raw->lifetime());
return result;
}
void ShowOrPremiumBox(
not_null<GenericBox*> box,
ShowOrPremium type,
QString shortName,
Fn<void()> justShow,
Fn<void()> toPremium) {
struct Skin {
rpl::producer<QString> showTitle;
rpl::producer<TextWithEntities> showAbout;
rpl::producer<QString> showButton;
rpl::producer<QString> orPremium;
rpl::producer<QString> premiumTitle;
rpl::producer<TextWithEntities> premiumAbout;
rpl::producer<QString> premiumButton;
QString toast;
QString lottie;
};
auto skin = (type == ShowOrPremium::LastSeen)
? Skin{
tr::lng_lastseen_show_title(),
tr::lng_lastseen_show_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_lastseen_show_button(),
tr::lng_lastseen_or(),
tr::lng_lastseen_premium_title(),
tr::lng_lastseen_premium_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_lastseen_premium_button(),
tr::lng_lastseen_shown_toast(tr::now),
u"show_or_premium_lastseen"_q,
}
: Skin{
tr::lng_readtime_show_title(),
tr::lng_readtime_show_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_readtime_show_button(),
tr::lng_readtime_or(),
tr::lng_readtime_premium_title(),
tr::lng_readtime_premium_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_readtime_premium_button(),
tr::lng_readtime_shown_toast(tr::now),
u"show_or_premium_readtime"_q,
};
box->setStyle(st::showOrBox);
box->setWidth(st::boxWideWidth);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
const auto buttonPadding = QMargins(
st::showOrBox.buttonPadding.left(),
0,
st::showOrBox.buttonPadding.right(),
0);
auto icon = Settings::CreateLottieIcon(
box,
{
.name = skin.lottie,
.sizeOverride = st::normalBoxLottieSize
- Size(st::showOrTitleIconMargin * 2),
},
{ 0, st::showOrTitleIconMargin, 0, st::showOrTitleIconMargin });
Settings::AddLottieIconWithCircle(
box->verticalLayout(),
std::move(icon.widget),
st::settingsBlockedListIconPadding,
st::normalBoxLottieSize);
Ui::AddSkip(box->verticalLayout());
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.showTitle),
st::boostCenteredTitle),
st::showOrTitlePadding + buttonPadding,
style::al_top);
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.showAbout),
st::boostText),
st::showOrAboutPadding + buttonPadding,
style::al_top);
const auto show = box->addRow(
object_ptr<RoundButton>(
box,
std::move(skin.showButton),
st::showOrShowButton),
buttonPadding);
show->setTextTransform(RoundButton::TextTransform::NoTransform);
box->addRow(
MakeShowOrLabel(box, std::move(skin.orPremium)),
st::showOrLabelPadding + buttonPadding,
style::al_justify);
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.premiumTitle),
st::boostCenteredTitle),
st::showOrTitlePadding + buttonPadding,
style::al_top);
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.premiumAbout),
st::boostText),
st::showOrPremiumAboutPadding + buttonPadding,
style::al_top);
const auto premium = CreateChild<GradientButton>(
box.get(),
Premium::ButtonGradientStops());
premium->resize(st::showOrShowButton.width, st::showOrShowButton.height);
const auto label = CreateChild<FlatLabel>(
premium,
std::move(skin.premiumButton),
st::premiumPreviewButtonLabel);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
rpl::combine(
premium->widthValue(),
label->widthValue()
) | rpl::on_next([=](int outer, int width) {
label->moveToLeft(
(outer - width) / 2,
st::premiumPreviewBox.button.textTop,
outer);
}, label->lifetime());
box->setShowFinishedCallback([=, animate = std::move(icon.animate)] {
premium->startGlareAnimation();
animate(anim::repeat::once);
});
box->addButton(
object_ptr<AbstractButton>::fromRaw(premium));
show->setClickedCallback([box, justShow, toast = skin.toast] {
justShow();
box->uiShow()->showToast(toast);
box->closeBox();
});
premium->setClickedCallback(std::move(toPremium));
}
} // namespace Ui

View File

@@ -0,0 +1,32 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class RpWidget;
class GenericBox;
enum class ShowOrPremium : uchar {
LastSeen,
ReadTime,
};
void ShowOrPremiumBox(
not_null<GenericBox*> box,
ShowOrPremium type,
QString shortName,
Fn<void()> justShow,
Fn<void()> toPremium);
[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(
not_null<RpWidget*> parent,
rpl::producer<QString> text);
} // namespace Ui

View File

@@ -0,0 +1,55 @@
/*
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 "ui/boxes/single_choice_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
void SingleChoiceBox(
not_null<Ui::GenericBox*> box,
SingleChoiceBoxArgs &&args) {
box->setTitle(std::move(args.title));
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
args.initialSelection);
const auto layout = box->verticalLayout();
layout->add(object_ptr<Ui::FixedHeightWidget>(
layout,
st::boxOptionListPadding.top() + st::autolockButton.margin.top()));
auto &&ints = ranges::views::ints(0, ranges::unreachable);
for (const auto &[i, text] : ranges::views::zip(ints, args.options)) {
layout->add(
object_ptr<Ui::Radiobutton>(
layout,
group,
i,
text,
args.st ? *args.st : st::defaultBoxCheckbox,
args.radioSt ? *args.radioSt : st::defaultRadio),
QMargins(
st::boxPadding.left() + st::boxOptionListPadding.left(),
0,
st::boxPadding.right(),
st::boxOptionListSkip));
}
const auto callback = args.callback.value();
group->setChangedCallback([=](int value) {
const auto weak = base::make_weak(box);
callback(value);
if (weak) {
box->closeBox();
}
});
}

View File

@@ -0,0 +1,32 @@
/*
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 "ui/layers/generic_box.h"
#include "base/required.h"
namespace style {
struct Checkbox;
struct Radio;
} // namespace style
struct SingleChoiceBoxArgs {
template <typename T>
using required = base::required<T>;
required<rpl::producer<QString>> title;
const std::vector<QString> &options;
int initialSelection = 0;
required<Fn<void(int)>> callback;
const style::Checkbox *st = nullptr;
const style::Radio *radioSt = nullptr;
};
void SingleChoiceBox(
not_null<Ui::GenericBox*> box,
SingleChoiceBoxArgs &&args);

View File

@@ -0,0 +1,133 @@
/*
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 "ui/boxes/time_picker_box.h"
#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "ui/layers/generic_box.h"
#include "ui/effects/animation_value.h"
#include "ui/ui_utility.h"
#include "ui/widgets/vertical_drum_picker.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
namespace Ui {
std::vector<TimeId> DefaultTimePickerValues() {
return {
(60 * 15),
(60 * 30),
(3600 * 1),
(3600 * 2),
(3600 * 3),
(3600 * 4),
(3600 * 8),
(3600 * 12),
(86400 * 1),
(86400 * 2),
(86400 * 3),
(86400 * 7 * 1),
(86400 * 7 * 2),
(86400 * 31 * 1),
(86400 * 31 * 2),
(86400 * 31 * 3),
};
}
Fn<TimeId()> TimePickerBox(
not_null<GenericBox*> box,
std::vector<TimeId> values,
std::vector<QString> phrases,
TimeId startValue) {
Expects(phrases.size() == values.size());
const auto startIndex = [&, &v = startValue] {
const auto it = ranges::lower_bound(values, v);
if (it == begin(values)) {
return 0;
}
const auto left = *(it - 1);
const auto right = *it;
const auto shift = (std::abs(v - left) < std::abs(v - right))
? -1
: 0;
return int(std::distance(begin(values), it - shift));
}();
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::historyMessagesTTLPickerHeight));
const auto font = st::boxTextFont;
const auto maxPhraseWidth = [&] {
// We have to use QFontMetricsF instead of
// FontData::width for more precise calculation.
const auto mf = QFontMetricsF(font->f);
const auto maxPhrase = ranges::max_element(
phrases,
std::less<>(),
[&](const QString &s) { return mf.horizontalAdvance(s); });
return std::ceil(mf.horizontalAdvance(*maxPhrase));
}();
const auto itemHeight = st::historyMessagesTTLPickerItemHeight;
auto paintCallback = Ui::VerticalDrumPicker::DefaultPaintCallback(
font,
itemHeight,
[=](QPainter &p, QRectF r, int index) {
p.drawText(r, phrases[index], style::al_center);
});
const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>(
content,
std::move(paintCallback),
phrases.size(),
itemHeight,
startIndex);
content->sizeValue(
) | rpl::on_next([=](const QSize &s) {
picker->resize(maxPhraseWidth, s.height());
picker->moveToLeft((s.width() - picker->width()) / 2, 0);
}, content->lifetime());
content->paintRequest(
) | rpl::on_next([=](const QRect &r) {
auto p = QPainter(content);
p.fillRect(r, Qt::transparent);
const auto lineRect = QRect(
0,
content->height() / 2,
content->width(),
st::defaultInputField.borderActive);
p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);
p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);
}, content->lifetime());
base::install_event_filter(content, [=](not_null<QEvent*> e) {
if ((e->type() == QEvent::MouseButtonPress)
|| (e->type() == QEvent::MouseButtonRelease)
|| (e->type() == QEvent::MouseMove)) {
picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get()));
} else if (e->type() == QEvent::Wheel) {
picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
base::install_event_filter(box, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
return [=] { return values[picker->index()]; };
}
} // namespace Ui

View File

@@ -0,0 +1,22 @@
/*
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
namespace Ui {
class GenericBox;
[[nodiscard]] std::vector<TimeId> DefaultTimePickerValues();
[[nodiscard]] Fn<TimeId()> TimePickerBox(
not_null<GenericBox*> box,
std::vector<TimeId> values,
std::vector<QString> phrases,
TimeId startValue);
} // namespace Ui

View File

@@ -0,0 +1,285 @@
/*
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 "ui/cached_round_corners.h"
#include "ui/chat/chat_style.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/image/image_prepare.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_overview.h"
#include "styles/style_media_view.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace {
constexpr auto kCachedCornerRadiusCount = int(CachedCornerRadius::kCount);
std::vector<CornersPixmaps> Corners;
QImage CornersMaskLarge[4], CornersMaskSmall[4];
rpl::lifetime PaletteChangedLifetime;
std::array<std::array<QImage, 4>, kCachedCornerRadiusCount> CachedMasks;
[[nodiscard]] std::array<QImage, 4> PrepareCorners(int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {
int32 r = radius * style::DevicePixelRatio(), s = st::msgShadow * style::DevicePixelRatio();
QImage rect(r * 3, r * 3 + (shadow ? s : 0), QImage::Format_ARGB32_Premultiplied);
rect.fill(Qt::transparent);
{
auto p = QPainter(&rect);
PainterHighQualityEnabler hq(p);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setPen(Qt::NoPen);
if (shadow) {
p.setBrush((*shadow)->b);
p.drawRoundedRect(0, s, r * 3, r * 3, r, r);
}
p.setBrush(brush);
p.drawRoundedRect(0, 0, r * 3, r * 3, r, r);
}
auto result = std::array<QImage, 4>();
result[0] = rect.copy(0, 0, r, r);
result[1] = rect.copy(r * 2, 0, r, r);
result[2] = rect.copy(0, r * 2, r, r + (shadow ? s : 0));
result[3] = rect.copy(r * 2, r * 2, r, r + (shadow ? s : 0));
return result;
}
void PrepareCorners(CachedRoundCorners index, int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {
Expects(index < Corners.size());
auto images = PrepareCorners(radius, brush, shadow);
for (int i = 0; i < 4; ++i) {
Corners[index].p[i] = PixmapFromImage(std::move(images[i]));
Corners[index].p[i].setDevicePixelRatio(style::DevicePixelRatio());
}
}
void CreateMaskCorners() {
auto mask = PrepareCorners(st::roundRadiusSmall, QColor(255, 255, 255), nullptr);
for (int i = 0; i < 4; ++i) {
CornersMaskSmall[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);
CornersMaskSmall[i].setDevicePixelRatio(style::DevicePixelRatio());
}
mask = PrepareCorners(st::roundRadiusLarge, QColor(255, 255, 255), nullptr);
for (int i = 0; i < 4; ++i) {
CornersMaskLarge[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);
CornersMaskLarge[i].setDevicePixelRatio(style::DevicePixelRatio());
}
}
void CreatePaletteCorners() {
PrepareCorners(MenuCorners, st::roundRadiusSmall, st::menuBg);
PrepareCorners(BoxCorners, st::boxRadius, st::boxBg);
PrepareCorners(DateCorners, st::dateRadius, st::msgDateImgBg);
PrepareCorners(OverviewVideoCorners, st::overviewVideoStatusRadius, st::msgDateImgBg);
PrepareCorners(OverviewVideoSelectedCorners, st::overviewVideoStatusRadius, st::msgDateImgBgSelected);
PrepareCorners(ForwardCorners, st::roundRadiusLarge, st::historyForwardChooseBg);
PrepareCorners(MediaviewSaveCorners, st::mediaviewControllerRadius, st::mediaviewSaveMsgBg);
PrepareCorners(StickerHoverCorners, st::roundRadiusSmall, st::emojiPanHover);
PrepareCorners(BotKeyboardCorners, st::roundRadiusSmall, st::botKbBg);
PrepareCorners(Doc1Corners, st::roundRadiusSmall, st::msgFile1Bg);
PrepareCorners(Doc2Corners, st::roundRadiusSmall, st::msgFile2Bg);
PrepareCorners(Doc3Corners, st::roundRadiusSmall, st::msgFile3Bg);
PrepareCorners(Doc4Corners, st::roundRadiusSmall, st::msgFile4Bg);
}
} // namespace
void StartCachedCorners() {
Corners.resize(RoundCornersCount);
CreateMaskCorners();
CreatePaletteCorners();
style::PaletteChanged(
) | rpl::on_next([=] {
CreatePaletteCorners();
}, PaletteChangedLifetime);
}
void FinishCachedCorners() {
Corners.clear();
PaletteChangedLifetime.destroy();
}
void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, const CornersPixmaps &corners) {
using namespace Images;
const auto fillBg = [&](QRect rect) {
p.fillRect(rect, bg);
};
const auto fillCorner = [&](int x, int y, int index) {
if (const auto &pix = corners.p[index]; !pix.isNull()) {
p.drawPixmap(x, y, pix);
}
};
if (corners.p[kTopLeft].isNull()
&& corners.p[kTopRight].isNull()
&& corners.p[kBottomLeft].isNull()
&& corners.p[kBottomRight].isNull()) {
p.fillRect(x, y, w, h, bg);
return;
}
const auto ratio = style::DevicePixelRatio();
const auto cornerSize = [&](int index) {
return corners.p[index].isNull()
? 0
: (corners.p[index].width() / ratio);
};
const auto verticalSkip = [&](int left, int right) {
return std::max(cornerSize(left), cornerSize(right));
};
const auto top = verticalSkip(kTopLeft, kTopRight);
const auto bottom = verticalSkip(kBottomLeft, kBottomRight);
if (top) {
const auto left = cornerSize(kTopLeft);
const auto right = cornerSize(kTopRight);
if (left) {
fillCorner(x, y, kTopLeft);
if (const auto add = top - left) {
fillBg({ x, y + left, left, add });
}
}
if (const auto fill = w - left - right; fill > 0) {
fillBg({ x + left, y, fill, top });
}
if (right) {
fillCorner(x + w - right, y, kTopRight);
if (const auto add = top - right) {
fillBg({ x + w - right, y + right, right, add });
}
}
}
if (const auto fill = h - top - bottom; fill > 0) {
fillBg({ x, y + top, w, fill });
}
if (bottom) {
const auto left = cornerSize(kBottomLeft);
const auto right = cornerSize(kBottomRight);
if (left) {
fillCorner(x, y + h - left, kBottomLeft);
if (const auto add = bottom - left) {
fillBg({ x, y + h - bottom, left, add });
}
}
if (const auto fill = w - left - right; fill > 0) {
fillBg({ x + left, y + h - bottom, fill, bottom });
}
if (right) {
fillCorner(x + w - right, y + h - right, kBottomRight);
if (const auto add = bottom - right) {
fillBg({ x + w - right, y + h - bottom, right, add });
}
}
}
}
void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, CachedRoundCorners index) {
FillRoundRect(p, x, y, w, h, bg, CachedCornerPixmaps(index));
}
void FillRoundShadow(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color shadow, const CornersPixmaps &corners) {
constexpr auto kLeft = 2;
constexpr auto kRight = 3;
const auto ratio = style::DevicePixelRatio();
const auto size = [&](int index) {
const auto &pix = corners.p[index];
return pix.isNull() ? 0 : (pix.width() / ratio);
};
const auto fillCorner = [&](int left, int bottom, int index) {
const auto &pix = corners.p[index];
if (pix.isNull()) {
return;
}
const auto size = pix.width() / ratio;
p.drawPixmap(left, bottom - size, pix);
};
const auto left = size(kLeft);
const auto right = size(kRight);
const auto from = x + left;
fillCorner(x, y + h + st::msgShadow, kLeft);
if (const auto width = w - left - right; width > 0) {
p.fillRect(from, y + h, width, st::msgShadow, shadow);
}
fillCorner(x + w - right, y + h + st::msgShadow, kRight);
}
const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index) {
Expects(index >= 0 && index < RoundCornersCount);
return Corners[index];
}
CornersPixmaps PrepareCornerPixmaps(int radius, style::color bg, const style::color *sh) {
auto images = PrepareCorners(radius, bg, sh);
auto result = CornersPixmaps();
for (int j = 0; j < 4; ++j) {
result.p[j] = PixmapFromImage(std::move(images[j]));
result.p[j].setDevicePixelRatio(style::DevicePixelRatio());
}
return result;
}
CornersPixmaps PrepareCornerPixmaps(ImageRoundRadius radius, style::color bg, const style::color *sh) {
switch (radius) {
case ImageRoundRadius::Small:
return PrepareCornerPixmaps(st::roundRadiusSmall, bg, sh);
case ImageRoundRadius::Large:
return PrepareCornerPixmaps(st::roundRadiusLarge, bg, sh);
}
Unexpected("Image round radius in PrepareCornerPixmaps.");
}
CornersPixmaps PrepareInvertedCornerPixmaps(int radius, style::color bg) {
const auto size = radius * style::DevicePixelRatio();
auto circle = style::colorizeImage(
style::createInvertedCircleMask(radius * 2),
bg);
circle.setDevicePixelRatio(style::DevicePixelRatio());
auto result = CornersPixmaps();
const auto fill = [&](int index, int xoffset, int yoffset) {
result.p[index] = PixmapFromImage(
circle.copy(QRect(xoffset, yoffset, size, size)));
};
fill(0, 0, 0);
fill(1, size, 0);
fill(2, size, size);
fill(3, 0, size);
return result;
}
[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag) {
using Radius = CachedCornerRadius;
switch (tag) {
case Radius::Small: return st::roundRadiusSmall;
case Radius::ThumbSmall: return MsgFileThumbRadiusSmall();
case Radius::ThumbLarge: return MsgFileThumbRadiusLarge();
case Radius::BubbleSmall: return BubbleRadiusSmall();
case Radius::BubbleLarge: return BubbleRadiusLarge();
}
Unexpected("Radius tag in CachedCornerRadiusValue.");
}
[[nodiscard]] const std::array<QImage, 4> &CachedCornersMasks(
CachedCornerRadius radius) {
const auto index = static_cast<int>(radius);
Assert(index >= 0 && index < kCachedCornerRadiusCount);
if (CachedMasks[index][0].isNull()) {
CachedMasks[index] = Images::CornersMask(
CachedCornerRadiusValue(CachedCornerRadius(index)));
}
return CachedMasks[index];
}
} // namespace Ui

View File

@@ -0,0 +1,82 @@
/*
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 "ui/rect_part.h"
enum class ImageRoundRadius;
namespace Ui {
struct CornersPixmaps {
QPixmap p[4];
};
enum CachedRoundCorners : int {
BoxCorners,
MenuCorners,
DateCorners,
OverviewVideoCorners,
OverviewVideoSelectedCorners,
ForwardCorners,
MediaviewSaveCorners,
StickerHoverCorners,
BotKeyboardCorners,
Doc1Corners,
Doc2Corners,
Doc3Corners,
Doc4Corners,
RoundCornersCount
};
void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, CachedRoundCorners index);
inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, CachedRoundCorners index) {
FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, index);
}
[[nodiscard]] const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index);
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
int radius,
style::color bg,
const style::color *sh = nullptr);
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
ImageRoundRadius radius,
style::color bg,
const style::color *sh = nullptr);
[[nodiscard]] CornersPixmaps PrepareInvertedCornerPixmaps(
int radius,
style::color bg);
void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, const CornersPixmaps &corners);
inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, const CornersPixmaps &corners) {
return FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, corners);
}
void FillRoundShadow(QPainter &p, int x, int y, int w, int h, style::color shadow, const CornersPixmaps &corners);
inline void FillRoundShadow(QPainter &p, const QRect &rect, style::color shadow, const CornersPixmaps &corners) {
FillRoundShadow(p, rect.x(), rect.y(), rect.width(), rect.height(), shadow, corners);
}
enum class CachedCornerRadius {
Small,
ThumbSmall,
ThumbLarge,
BubbleSmall,
BubbleLarge,
kCount,
};
[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag);
[[nodiscard]] const std::array<QImage, 4> &CachedCornersMasks(
CachedCornerRadius radius);
void StartCachedCorners();
void FinishCachedCorners();
} // namespace Ui

View File

@@ -0,0 +1,205 @@
/*
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 "ui/chat/attach/attach_abstract_single_file_preview.h"
#include "base/timer_rpl.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/text/text_options.h"
#include "ui/ui_utility.h"
#include "ui/widgets/buttons.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
AbstractSingleFilePreview::AbstractSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type)
: AbstractSinglePreview(parent)
, _st(st)
, _type(type)
, _editMedia(this, _st.files.buttonFile)
, _deleteMedia(this, _st.files.buttonFile) {
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
if (type == AttachControls::Type::Full) {
_deleteMedia->show();
_editMedia->show();
} else if (type == AttachControls::Type::EditOnly) {
_deleteMedia->hide();
_editMedia->show();
} else if (type == AttachControls::Type::None) {
_deleteMedia->hide();
_editMedia->hide();
}
}
AbstractSingleFilePreview::~AbstractSingleFilePreview() = default;
rpl::producer<> AbstractSingleFilePreview::editRequests() const {
return _editMedia->clicks() | rpl::map([] {
return base::timer_once(st::historyAttach.ripple.hideDuration);
}) | rpl::flatten_latest();
}
rpl::producer<> AbstractSingleFilePreview::deleteRequests() const {
return _deleteMedia->clicks() | rpl::to_empty;
}
rpl::producer<> AbstractSingleFilePreview::modifyRequests() const {
return rpl::never<>();
}
rpl::producer<> AbstractSingleFilePreview::editCoverRequests() const {
return rpl::never<>();
}
rpl::producer<> AbstractSingleFilePreview::clearCoverRequests() const {
return rpl::never<>();
}
void AbstractSingleFilePreview::prepareThumbFor(
Data &data,
const QImage &preview) {
if (preview.isNull()) {
return;
}
auto originalWidth = preview.width();
auto originalHeight = preview.height();
const auto &st = st::attachPreviewThumbLayout;
auto thumbWidth = st.thumbSize;
if (originalWidth > originalHeight) {
thumbWidth = (originalWidth * st.thumbSize) / originalHeight;
}
const auto options = Images::Option::RoundSmall;
data.fileThumb = PixmapFromImage(Images::Prepare(
preview,
thumbWidth * style::DevicePixelRatio(),
{ .options = options, .outer = { st.thumbSize, st.thumbSize } }));
}
void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto w = width()
- st::boxPhotoPadding.left()
- st::boxPhotoPadding.right();
const auto &st = !isThumbedLayout(_data)
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
const auto nameleft = st.thumbSize + st.thumbSkip;
const auto nametop = st.nameTop;
const auto statustop = st.statusTop;
const auto x = (width() - w) / 2, y = 0;
if (!isThumbedLayout(_data)) {
QRect inner(
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
p.setPen(Qt::NoPen);
if (_data.fileIsAudio && !_data.fileThumb.isNull()) {
p.drawPixmap(inner.topLeft(), _data.fileThumb);
} else {
p.setBrush(_st.files.iconBg);
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
auto &icon = _data.fileIsAudio
? (_data.fileThumb.isNull()
? _st.files.iconPlay
: st::historyFileThumbPlay)
: _data.fileIsImage
? _st.files.iconImage
: _st.files.iconDocument;
icon.paintInCenter(p, inner);
} else {
QRect rthumb(
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
p.drawPixmap(rthumb.topLeft(), _data.fileThumb);
}
p.setFont(st::semiboldFont);
p.setPen(_st.files.nameFg);
p.drawTextLeft(
x + nameleft,
y + nametop, width(),
_data.name,
_data.nameWidth);
p.setFont(st::normalFont);
p.setPen(_st.files.statusFg);
p.drawTextLeft(
x + nameleft,
y + statustop,
width(),
_data.statusText,
_data.statusWidth);
}
void AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) {
const auto w = width()
- st::boxPhotoPadding.left()
- st::boxPhotoPadding.right();
const auto x = (width() - w) / 2;
const auto top = st::sendBoxFileGroupSkipTop;
auto right = st::sendBoxFileGroupSkipRight + x;
if (_type != AttachControls::Type::EditOnly) {
_deleteMedia->moveToRight(right, top);
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
}
_editMedia->moveToRight(right, top);
}
bool AbstractSingleFilePreview::isThumbedLayout(Data &data) const {
return (!data.fileThumb.isNull() && !data.fileIsAudio);
}
void AbstractSingleFilePreview::updateTextWidthFor(Data &data) {
const auto &st = !isThumbedLayout(data)
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
const auto buttonsCount = (_type == AttachControls::Type::EditOnly)
? 1
: (_type == AttachControls::Type::Full)
? 2
: 0;
const auto availableFileWidth = st::sendMediaPreviewSize
- st.thumbSize
- st.thumbSkip
// Right buttons.
- _st.files.buttonFile.width * buttonsCount
- st::sendBoxAlbumGroupEditInternalSkip * buttonsCount
- st::sendBoxAlbumGroupSkipRight;
data.nameWidth = st::semiboldFont->width(data.name);
if (data.nameWidth > availableFileWidth) {
data.name = st::semiboldFont->elided(
data.name,
availableFileWidth,
Qt::ElideMiddle);
data.nameWidth = st::semiboldFont->width(data.name);
}
data.statusWidth = st::normalFont->width(data.statusText);
}
void AbstractSingleFilePreview::setData(const Data &data) {
_data = data;
updateTextWidthFor(_data);
const auto &st = !isThumbedLayout(_data)
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
resize(width(), st.thumbSize);
}
} // namespace Ui

View File

@@ -0,0 +1,68 @@
/*
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 "ui/chat/attach/attach_abstract_single_preview.h"
#include "ui/chat/attach/attach_controls.h"
#include "base/object_ptr.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
class IconButton;
class AbstractSingleFilePreview : public AbstractSinglePreview {
public:
AbstractSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type);
~AbstractSingleFilePreview();
[[nodiscard]] rpl::producer<> deleteRequests() const override;
[[nodiscard]] rpl::producer<> editRequests() const override;
[[nodiscard]] rpl::producer<> modifyRequests() const override;
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
protected:
struct Data {
QPixmap fileThumb;
QString name;
QString statusText;
int nameWidth = 0;
int statusWidth = 0;
bool fileIsAudio = false;
bool fileIsImage = false;
};
void prepareThumbFor(Data &data, const QImage &preview);
bool isThumbedLayout(Data &data) const;
void setData(const Data &data);
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void updateTextWidthFor(Data &data);
const style::ComposeControls &_st;
const AttachControls::Type _type;
Data _data;
object_ptr<IconButton> _editMedia = { nullptr };
object_ptr<IconButton> _deleteMedia = { nullptr };
};
} // namespace Ui

View File

@@ -0,0 +1,344 @@
/*
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 "ui/chat/attach/attach_abstract_single_media_preview.h"
#include "editor/photo_editor_common.h"
#include "lang/lang_keys.h"
#include "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace Ui {
namespace {
constexpr auto kMinPreviewWidth = 20;
} // namespace
AbstractSingleMediaPreview::AbstractSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed)
: AbstractSinglePreview(parent)
, _st(st)
, _actionAllowed(std::move(actionAllowed))
, _minThumbH(st::sendBoxAlbumGroupSize.height()
+ st::sendBoxAlbumGroupSkipTop * 2)
, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {
}
AbstractSingleMediaPreview::~AbstractSingleMediaPreview() = default;
rpl::producer<> AbstractSingleMediaPreview::deleteRequests() const {
return _controls->deleteRequests();
}
rpl::producer<> AbstractSingleMediaPreview::editRequests() const {
return _controls->editRequests();
}
rpl::producer<> AbstractSingleMediaPreview::modifyRequests() const {
return _photoEditorRequests.events();
}
rpl::producer<> AbstractSingleMediaPreview::editCoverRequests() const {
return _editCoverRequests.events();
}
rpl::producer<> AbstractSingleMediaPreview::clearCoverRequests() const {
return _clearCoverRequests.events();
}
void AbstractSingleMediaPreview::setSendWay(SendFilesWay way) {
_sendWay = way;
update();
}
SendFilesWay AbstractSingleMediaPreview::sendWay() const {
return _sendWay;
}
void AbstractSingleMediaPreview::setSpoiler(bool spoiler) {
_spoiler = spoiler
? std::make_unique<SpoilerAnimation>([=] { update(); })
: nullptr;
update();
}
bool AbstractSingleMediaPreview::hasSpoiler() const {
return _spoiler != nullptr;
}
bool AbstractSingleMediaPreview::canHaveSpoiler() const {
return supportsSpoilers();
}
rpl::producer<bool> AbstractSingleMediaPreview::spoileredChanges() const {
return _spoileredChanges.events();
}
QImage AbstractSingleMediaPreview::generatePriceTagBackground() const {
return (_previewBlurred.isNull() ? _preview : _previewBlurred).toImage();
}
void AbstractSingleMediaPreview::preparePreview(QImage preview) {
auto maxW = 0;
auto maxH = 0;
if (_animated && drawBackground()) {
auto limitW = st::sendMediaPreviewSize;
auto limitH = st::confirmMaxHeight;
maxW = qMax(preview.width(), 1);
maxH = qMax(preview.height(), 1);
if (maxW * limitH > maxH * limitW) {
if (maxW < limitW) {
maxH = maxH * limitW / maxW;
maxW = limitW;
}
} else {
if (maxH < limitH) {
maxW = maxW * limitH / maxH;
maxH = limitH;
}
}
const auto ratio = style::DevicePixelRatio();
preview = Images::Prepare(
std::move(preview),
QSize(maxW, maxH) * ratio,
{ .outer = { maxW, maxH } });
}
auto originalWidth = preview.width();
auto originalHeight = preview.height();
if (!originalWidth || !originalHeight) {
originalWidth = originalHeight = 1;
}
_previewWidth = st::sendMediaPreviewSize;
if (preview.width() < _previewWidth) {
_previewWidth = qMax(preview.width(), kMinPreviewWidth);
}
auto maxthumbh = qMin(qRound(1.5 * _previewWidth), st::confirmMaxHeight);
_previewHeight = qRound(originalHeight
* float64(_previewWidth)
/ originalWidth);
if (_previewHeight > maxthumbh) {
_previewWidth = qRound(_previewWidth
* float64(maxthumbh)
/ _previewHeight);
accumulate_max(_previewWidth, kMinPreviewWidth);
_previewHeight = maxthumbh;
}
_previewLeft = (st::boxWideWidth - _previewWidth) / 2;
if (_previewHeight < _minThumbH) {
_previewTop = (_minThumbH - _previewHeight) / 2;
}
preview = std::move(preview).scaled(
_previewWidth * style::DevicePixelRatio(),
_previewHeight * style::DevicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
preview = Images::Opaque(std::move(preview));
_preview = PixmapFromImage(std::move(preview));
_preview.setDevicePixelRatio(style::DevicePixelRatio());
_previewBlurred = QPixmap();
resize(width(), std::max(_previewHeight, _minThumbH));
}
bool AbstractSingleMediaPreview::isOverPreview(QPoint position) const {
return QRect(
_previewLeft,
_previewTop,
_previewWidth,
_previewHeight).contains(position);
}
void AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {
_controls->moveToRight(
st::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,
st::sendBoxAlbumGroupSkipTop,
width());
}
void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto takenSpoiler = supportsSpoilers()
? nullptr
: base::take(_spoiler);
const auto guard = gsl::finally([&] {
if (takenSpoiler) {
_spoiler = base::take(takenSpoiler);
}
});
if (drawBackground()) {
const auto &padding = st::boxPhotoPadding;
if (_previewLeft > padding.left()) {
p.fillRect(
padding.left(),
_previewTop,
_previewLeft - padding.left(),
_previewHeight,
_st.files.confirmBg);
}
if ((_previewLeft + _previewWidth) < (width() - padding.right())) {
p.fillRect(
_previewLeft + _previewWidth,
_previewTop,
width() - padding.right() - _previewLeft - _previewWidth,
_previewHeight,
_st.files.confirmBg);
}
if (_previewTop > 0) {
p.fillRect(
padding.left(),
0,
width() - padding.right() - padding.left(),
height(),
_st.files.confirmBg);
}
}
if (_spoiler && _previewBlurred.isNull()) {
_previewBlurred = BlurredPreviewFromPixmap(_preview, RectPart::None);
}
if (_spoiler || !tryPaintAnimation(p)) {
const auto &pixmap = _spoiler ? _previewBlurred : _preview;
const auto position = QPoint(_previewLeft, _previewTop);
p.drawPixmap(position, pixmap);
if (_spoiler) {
const auto paused = On(PowerSaving::kChatSpoiler);
FillSpoilerRect(
p,
QRect(position, pixmap.size() / pixmap.devicePixelRatio()),
DefaultImageSpoiler().frame(
_spoiler->index(crl::now(), paused)));
}
}
if (_animated && !isAnimatedPreviewReady() && !_spoiler) {
const auto innerSize = st::msgFileLayout.thumbSize;
auto inner = QRect(
_previewLeft + (_previewWidth - innerSize) / 2,
_previewTop + (_previewHeight - innerSize) / 2,
innerSize,
innerSize);
p.setPen(Qt::NoPen);
p.setBrush(st::msgDateImgBg);
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
auto icon = &st::historyFileInPlay;
icon->paintInCenter(p, inner);
}
}
void AbstractSingleMediaPreview::mousePressEvent(QMouseEvent *e) {
if (isOverPreview(e->pos())) {
_pressed = true;
}
}
void AbstractSingleMediaPreview::mouseMoveEvent(QMouseEvent *e) {
applyCursor((isPhoto() && isOverPreview(e->pos()))
? style::cur_pointer
: style::cur_default);
}
void AbstractSingleMediaPreview::mouseReleaseEvent(QMouseEvent *e) {
if (base::take(_pressed) && isOverPreview(e->pos())) {
if (e->button() == Qt::RightButton) {
showContextMenu(e->globalPos());
} else if (isPhoto()) {
_photoEditorRequests.fire({});
}
}
}
void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) {
if (_cursor != cursor) {
_cursor = cursor;
setCursor(_cursor);
}
}
void AbstractSingleMediaPreview::showContextMenu(QPoint position) {
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
_st.tabbed.menu);
const auto &icons = _st.tabbed.icons;
if (_actionAllowed(AttachActionType::ToggleSpoiler)
&& _sendWay.sendImagesAsPhotos()
&& supportsSpoilers()) {
const auto spoilered = hasSpoiler();
_menu->addAction(spoilered
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now), [=] {
setSpoiler(!spoilered);
_spoileredChanges.fire_copy(!spoilered);
}, spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
}
if (_actionAllowed(AttachActionType::EditCover)) {
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
_editCoverRequests.fire({});
}, &st::menuIconEdit);
if (_actionAllowed(AttachActionType::ClearCover)) {
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
_clearCoverRequests.fire({});
}, &st::menuIconCancel);
}
}
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(position);
}
}
int AbstractSingleMediaPreview::previewLeft() const {
return _previewLeft;
}
int AbstractSingleMediaPreview::previewTop() const {
return _previewTop;
}
int AbstractSingleMediaPreview::previewWidth() const {
return _previewWidth;
}
int AbstractSingleMediaPreview::previewHeight() const {
return _previewHeight;
}
void AbstractSingleMediaPreview::setAnimated(bool animated) {
_animated = animated;
}
bool AbstractSingleMediaPreview::isPhoto() const {
return drawBackground()
&& !isAnimatedPreviewReady()
&& !_animated;
}
} // namespace Ui

View File

@@ -0,0 +1,106 @@
/*
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 "ui/chat/attach/attach_abstract_single_preview.h"
#include "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/abstract_button.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
class PopupMenu;
class AbstractSingleMediaPreview : public AbstractSinglePreview {
public:
AbstractSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed);
~AbstractSingleMediaPreview();
void setSendWay(SendFilesWay way);
[[nodiscard]] SendFilesWay sendWay() const;
[[nodiscard]] rpl::producer<> deleteRequests() const override;
[[nodiscard]] rpl::producer<> editRequests() const override;
[[nodiscard]] rpl::producer<> modifyRequests() const override;
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
[[nodiscard]] bool isPhoto() const;
void setSpoiler(bool spoiler);
[[nodiscard]] bool hasSpoiler() const;
[[nodiscard]] bool canHaveSpoiler() const;
[[nodiscard]] rpl::producer<bool> spoileredChanges() const;
[[nodiscard]] QImage generatePriceTagBackground() const;
protected:
virtual bool supportsSpoilers() const = 0;
virtual bool drawBackground() const = 0;
virtual bool tryPaintAnimation(QPainter &p) = 0;
virtual bool isAnimatedPreviewReady() const = 0;
void preparePreview(QImage preview);
int previewLeft() const;
int previewTop() const;
int previewWidth() const;
int previewHeight() const;
void setAnimated(bool animated);
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
[[nodiscard]] bool isOverPreview(QPoint position) const;
void applyCursor(style::cursor cursor);
void showContextMenu(QPoint position);
const style::ComposeControls &_st;
SendFilesWay _sendWay;
Fn<bool(AttachActionType)> _actionAllowed;
bool _animated = false;
QPixmap _preview;
QPixmap _previewBlurred;
int _previewLeft = 0;
int _previewTop = 0;
int _previewWidth = 0;
int _previewHeight = 0;
std::unique_ptr<SpoilerAnimation> _spoiler;
rpl::event_stream<bool> _spoileredChanges;
const int _minThumbH;
const base::unique_qptr<AttachControlsWidget> _controls;
rpl::event_stream<> _photoEditorRequests;
rpl::event_stream<> _editCoverRequests;
rpl::event_stream<> _clearCoverRequests;
style::cursor _cursor = style::cur_default;
bool _pressed = false;
base::unique_qptr<PopupMenu> _menu;
rpl::event_stream<> _modifyRequests;
};
} // namespace Ui

View File

@@ -0,0 +1,26 @@
/*
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 "ui/rp_widget.h"
namespace Ui {
class AbstractSinglePreview : public RpWidget {
public:
using RpWidget::RpWidget;
[[nodiscard]] virtual rpl::producer<> deleteRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> editRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> modifyRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> editCoverRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> clearCoverRequests() const = 0;
};
} // namespace Ui

View File

@@ -0,0 +1,675 @@
/*
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 "ui/chat/attach/attach_album_preview.h"
#include "ui/chat/attach/attach_album_thumbnail.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
namespace Media::Streaming {
[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);
} // namespace Media::Streaming
namespace Ui {
namespace {
constexpr auto kDragDuration = crl::time(200);
} // namespace
AlbumPreview::AlbumPreview(
QWidget *parent,
const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items,
SendFilesWay way,
Fn<bool(int, AttachActionType)> actionAllowed)
: RpWidget(parent)
, _st(st)
, _sendWay(way)
, _actionAllowed(std::move(actionAllowed))
, _dragTimer([=] { switchToDrag(); }) {
setMouseTracking(true);
prepareThumbs(items);
updateSize();
updateFileRows();
}
AlbumPreview::~AlbumPreview() = default;
void AlbumPreview::setSendWay(SendFilesWay way) {
if (_sendWay != way) {
cancelDrag();
_sendWay = way;
}
updateSize();
updateFileRows();
update();
}
void AlbumPreview::updateFileRows() {
Expects(_order.size() == _thumbs.size());
const auto isFile = !_sendWay.sendImagesAsPhotos();
auto top = 0;
for (auto i = 0; i < _order.size(); i++) {
const auto &thumb = _thumbs[_order[i]];
thumb->setButtonVisible(isFile && !thumb->isCompressedSticker());
thumb->moveButtons(top);
top += thumb->fileHeight() + st::sendMediaRowSkip;
}
}
base::flat_set<int> AlbumPreview::collectSpoileredIndices() {
auto result = base::flat_set<int>();
result.reserve(_thumbs.size());
auto i = 0;
for (const auto &thumb : _thumbs) {
if (thumb->hasSpoiler()) {
result.emplace(i);
}
++i;
}
return result;
}
bool AlbumPreview::canHaveSpoiler(int index) const {
return _sendWay.sendImagesAsPhotos();
}
void AlbumPreview::toggleSpoilers(bool enabled) {
for (auto &thumb : _thumbs) {
thumb->setSpoiler(enabled);
}
}
std::vector<int> AlbumPreview::takeOrder() {
//Expects(_thumbs.size() == _order.size());
//Expects(_itemsShownDimensions.size() == _order.size());
auto reordered = std::vector<std::unique_ptr<AlbumThumbnail>>();
auto reorderedShownDimensions = std::vector<QSize>();
reordered.reserve(_thumbs.size());
reorderedShownDimensions.reserve(_itemsShownDimensions.size());
for (auto index : _order) {
reordered.push_back(std::move(_thumbs[index]));
reorderedShownDimensions.push_back(_itemsShownDimensions[index]);
}
_thumbs = std::move(reordered);
_itemsShownDimensions = std::move(reorderedShownDimensions);
return std::exchange(_order, defaultOrder());
}
auto AlbumPreview::generateOrderedLayout() const
-> std::vector<GroupMediaLayout> {
auto layout = LayoutMediaGroup(
_itemsShownDimensions,
st::sendMediaPreviewSize,
st::historyGroupWidthMin / 2,
st::historyGroupSkip / 2);
Assert(layout.size() == _order.size());
return layout;
}
std::vector<int> AlbumPreview::defaultOrder(int count) const {
if (count < 0) {
count = _order.size();
}
return ranges::views::ints(0, count) | ranges::to_vector;
}
void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {
_order = defaultOrder(items.size());
_itemsShownDimensions = ranges::views::all(
_order
) | ranges::views::transform([&](int index) {
return items[index].shownDimensions;
}) | ranges::to_vector;
const auto count = int(_order.size());
const auto layout = generateOrderedLayout();
_thumbs.reserve(count);
for (auto i = 0; i != count; ++i) {
_thumbs.push_back(std::make_unique<AlbumThumbnail>(
_st,
items[i],
layout[i],
this,
[=] { update(); },
[=] { changeThumbByIndex(orderIndex(thumbUnderCursor())); },
[=] { deleteThumbByIndex(orderIndex(thumbUnderCursor())); }));
if (_thumbs.back()->isCompressedSticker()) {
_hasMixedFileHeights = true;
}
}
_thumbsHeight = countLayoutHeight(layout);
_photosHeight = ranges::accumulate(ranges::views::all(
_thumbs
) | ranges::views::transform([](const auto &thumb) {
return thumb->photoHeight();
}), 0) + (count - 1) * st::sendMediaRowSkip;
if (!_hasMixedFileHeights) {
_filesHeight = count * _thumbs.front()->fileHeight()
+ (count - 1) * st::sendMediaRowSkip;
} else {
_filesHeight = ranges::accumulate(ranges::views::all(
_thumbs
) | ranges::views::transform([](const auto &thumb) {
return thumb->fileHeight();
}), 0) + (count - 1) * st::sendMediaRowSkip;
}
}
int AlbumPreview::contentLeft() const {
return (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
}
int AlbumPreview::contentTop() const {
return 0;
}
AlbumThumbnail *AlbumPreview::findThumb(QPoint position) const {
position -= QPoint(contentLeft(), contentTop());
auto top = 0;
const auto isPhotosWay = _sendWay.sendImagesAsPhotos();
const auto skip = st::sendMediaRowSkip;
auto find = [&](const auto &thumb) {
if (_sendWay.groupFiles() && _sendWay.sendImagesAsPhotos()) {
return thumb->containsPoint(position);
} else {
const auto bottom = top + (isPhotosWay
? thumb->photoHeight()
: thumb->fileHeight());
const auto isUnderTop = (position.y() > top);
top = bottom + skip;
return isUnderTop && (position.y() < bottom);
}
return false;
};
const auto i = ranges::find_if(_thumbs, std::move(find));
return (i == _thumbs.end()) ? nullptr : i->get();
}
not_null<AlbumThumbnail*> AlbumPreview::findClosestThumb(
QPoint position) const {
Expects(_draggedThumb != nullptr);
if (const auto exact = findThumb(position)) {
return exact;
}
auto result = _draggedThumb;
auto distance = _draggedThumb->distanceTo(position);
for (const auto &thumb : _thumbs) {
const auto check = thumb->distanceTo(position);
if (check < distance) {
distance = check;
result = thumb.get();
}
}
return result;
}
int AlbumPreview::orderIndex(not_null<AlbumThumbnail*> thumb) const {
const auto i = ranges::find_if(_order, [&](int index) {
return (_thumbs[index].get() == thumb);
});
Assert(i != _order.end());
return int(i - _order.begin());
}
void AlbumPreview::cancelDrag() {
_thumbsHeightAnimation.stop();
_finishDragAnimation.stop();
_shrinkAnimation.stop();
if (_draggedThumb) {
_draggedThumb->moveInAlbum({ 0, 0 });
_draggedThumb = nullptr;
}
if (_suggestedThumb) {
const auto suggestedIndex = orderIndex(_suggestedThumb);
if (suggestedIndex > 0) {
_thumbs[_order[suggestedIndex - 1]]->suggestMove(0., [] {});
}
if (suggestedIndex < int(_order.size() - 1)) {
_thumbs[_order[suggestedIndex + 1]]->suggestMove(0., [] {});
}
_suggestedThumb->suggestMove(0., [] {});
_suggestedThumb->finishAnimations();
_suggestedThumb = nullptr;
}
_paintedAbove = nullptr;
update();
}
void AlbumPreview::finishDrag() {
Expects(_draggedThumb != nullptr);
Expects(_suggestedThumb != nullptr);
if (_suggestedThumb != _draggedThumb) {
const auto currentIndex = orderIndex(_draggedThumb);
const auto newIndex = orderIndex(_suggestedThumb);
const auto delta = (currentIndex < newIndex) ? 1 : -1;
const auto realIndex = _order[currentIndex];
for (auto i = currentIndex; i != newIndex; i += delta) {
_order[i] = _order[i + delta];
}
_order[newIndex] = realIndex;
const auto layout = generateOrderedLayout();
for (auto i = 0, count = int(_order.size()); i != count; ++i) {
_thumbs[_order[i]]->moveToLayout(layout[i]);
}
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
updateSizeAnimated(layout);
_orderUpdated.fire({});
} else {
for (const auto &thumb : _thumbs) {
thumb->resetLayoutAnimation();
}
_draggedThumb->animateLayoutToInitial();
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
}
}
int AlbumPreview::countLayoutHeight(
const std::vector<GroupMediaLayout> &layout) const {
const auto accumulator = [](int current, const auto &item) {
return std::max(current, item.geometry.y() + item.geometry.height());
};
return ranges::accumulate(layout, 0, accumulator);
}
void AlbumPreview::updateSizeAnimated(
const std::vector<GroupMediaLayout> &layout) {
const auto newHeight = countLayoutHeight(layout);
if (newHeight != _thumbsHeight) {
_thumbsHeightAnimation.start(
[=] { updateSize(); },
_thumbsHeight,
newHeight,
kDragDuration);
_thumbsHeight = newHeight;
}
}
void AlbumPreview::updateSize() {
const auto newHeight = [&] {
if (!_sendWay.sendImagesAsPhotos()) {
return _filesHeight;
} else if (!_sendWay.groupFiles()) {
return _photosHeight;
} else {
return int(base::SafeRound(_thumbsHeightAnimation.value(
_thumbsHeight)));
}
}();
if (height() != newHeight) {
resize(st::boxWideWidth, newHeight);
}
}
void AlbumPreview::paintEvent(QPaintEvent *e) {
Painter p(this);
if (!_sendWay.sendImagesAsPhotos()) {
paintFiles(p, e->rect());
} else if (!_sendWay.groupFiles()) {
paintPhotos(p, e->rect());
} else {
paintAlbum(p);
}
}
void AlbumPreview::paintAlbum(Painter &p) const {
const auto shrink = _shrinkAnimation.value(_draggedThumb ? 1. : 0.);
const auto moveProgress = _finishDragAnimation.value(1.);
const auto left = contentLeft();
const auto top = contentTop();
for (const auto &thumb : _thumbs) {
if (thumb.get() != _paintedAbove) {
thumb->paintInAlbum(p, left, top, shrink, moveProgress);
}
}
if (_paintedAbove) {
_paintedAbove->paintInAlbum(p, left, top, shrink, moveProgress);
}
}
void AlbumPreview::paintPhotos(Painter &p, QRect clip) const {
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
auto top = 0;
const auto outerWidth = width();
for (const auto &thumb : _thumbs) {
const auto bottom = top + thumb->photoHeight();
const auto guard = gsl::finally([&] {
top = bottom + st::sendMediaRowSkip;
});
if (top >= clip.y() + clip.height()) {
break;
} else if (bottom <= clip.y()) {
continue;
}
thumb->paintPhoto(p, left, top, outerWidth);
}
}
void AlbumPreview::paintFiles(Painter &p, QRect clip) const {
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
const auto outerWidth = width();
if (!_hasMixedFileHeights) {
const auto fileHeight = st::attachPreviewThumbLayout.thumbSize
+ st::sendMediaRowSkip;
const auto bottom = clip.y() + clip.height();
const auto from = std::clamp(
clip.y() / fileHeight,
0,
int(_thumbs.size()));
const auto till = std::clamp(
(bottom + fileHeight - 1) / fileHeight,
0,
int(_thumbs.size()));
auto top = from * fileHeight;
for (auto i = from; i != till; ++i) {
_thumbs[i]->paintFile(p, left, top, outerWidth);
top += fileHeight;
}
} else {
auto top = 0;
for (const auto &thumb : _thumbs) {
const auto bottom = top + thumb->fileHeight();
const auto guard = gsl::finally([&] {
top = bottom + st::sendMediaRowSkip;
});
if (top >= clip.y() + clip.height()) {
break;
} else if (bottom <= clip.y()) {
continue;
}
thumb->paintFile(p, left, top, outerWidth);
}
}
}
AlbumThumbnail *AlbumPreview::thumbUnderCursor() {
return findThumb(mapFromGlobal(QCursor::pos()));
}
void AlbumPreview::deleteThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbDeleted.fire(std::move(index));
}
void AlbumPreview::changeThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbChanged.fire(std::move(index));
}
void AlbumPreview::modifyThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbModified.fire(std::move(index));
}
void AlbumPreview::thumbButtonsCallback(
not_null<AlbumThumbnail*> thumb,
AttachButtonType type) {
const auto index = orderIndex(thumb);
switch (type) {
case AttachButtonType::None: return;
case AttachButtonType::Edit: changeThumbByIndex(index); break;
case AttachButtonType::Delete: deleteThumbByIndex(index); break;
case AttachButtonType::Modify:
cancelDrag();
modifyThumbByIndex(index);
break;
}
}
void AlbumPreview::mousePressEvent(QMouseEvent *e) {
if (_finishDragAnimation.animating()) {
return;
}
const auto position = e->pos();
cancelDrag();
if (const auto thumb = findThumb(position)) {
_draggedStartPosition = position;
_pressedThumb = thumb;
_pressedButtonType = thumb->buttonTypeFromPoint(position);
const auto isAlbum = _sendWay.sendImagesAsPhotos()
&& _sendWay.groupFiles();
if (!isAlbum || e->button() != Qt::LeftButton) {
_dragTimer.cancel();
return;
}
if (_pressedButtonType == AttachButtonType::None) {
switchToDrag();
} else if (_pressedButtonType == AttachButtonType::Modify) {
_dragTimer.callOnce(QApplication::startDragTime());
}
}
}
void AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
if (!_sendWay.sendImagesAsPhotos() && !_hasMixedFileHeights) {
applyCursor(style::cur_default);
return;
}
if (_dragTimer.isActive()) {
_dragTimer.cancel();
switchToDrag();
}
const auto isAlbum = _sendWay.sendImagesAsPhotos()
&& _sendWay.groupFiles();
if (isAlbum && _draggedThumb) {
const auto position = e->pos();
_draggedThumb->moveInAlbum(position - _draggedStartPosition);
updateSuggestedDrag(_draggedThumb->center());
update();
} else {
const auto thumb = findThumb(e->pos());
const auto regularCursor = isAlbum
? style::cur_pointer
: style::cur_default;
const auto cursor = thumb
? (thumb->buttonsContainPoint(e->pos())
? style::cur_pointer
: regularCursor)
: style::cur_default;
applyCursor(cursor);
}
}
void AlbumPreview::applyCursor(style::cursor cursor) {
if (_cursor != cursor) {
_cursor = cursor;
setCursor(_cursor);
}
}
void AlbumPreview::updateSuggestedDrag(QPoint position) {
auto closest = findClosestThumb(position);
auto closestIndex = orderIndex(closest);
const auto draggedIndex = orderIndex(_draggedThumb);
const auto closestIsBeforePoint = closest->isPointAfter(position);
if (closestIndex < draggedIndex && closestIsBeforePoint) {
closest = _thumbs[_order[++closestIndex]].get();
} else if (closestIndex > draggedIndex && !closestIsBeforePoint) {
closest = _thumbs[_order[--closestIndex]].get();
}
if (_suggestedThumb == closest) {
return;
}
const auto last = int(_order.size()) - 1;
if (_suggestedThumb) {
const auto suggestedIndex = orderIndex(_suggestedThumb);
if (suggestedIndex < draggedIndex && suggestedIndex > 0) {
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
previous->suggestMove(0., [=] { update(); });
} else if (suggestedIndex > draggedIndex && suggestedIndex < last) {
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
next->suggestMove(0., [=] { update(); });
}
_suggestedThumb->suggestMove(0., [=] { update(); });
}
_suggestedThumb = closest;
const auto suggestedIndex = closestIndex;
if (_suggestedThumb != _draggedThumb) {
const auto delta = (suggestedIndex < draggedIndex) ? 1. : -1.;
if (delta > 0. && suggestedIndex > 0) {
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
previous->suggestMove(-delta, [=] { update(); });
} else if (delta < 0. && suggestedIndex < last) {
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
next->suggestMove(-delta, [=] { update(); });
}
_suggestedThumb->suggestMove(delta, [=] { update(); });
}
}
void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
if (_draggedThumb) {
finishDrag();
_shrinkAnimation.start(
[=] { update(); },
1.,
0.,
AlbumThumbnail::kShrinkDuration);
_draggedThumb = nullptr;
_suggestedThumb = nullptr;
update();
} else if (const auto thumb = base::take(_pressedThumb)) {
const auto was = _pressedButtonType;
const auto now = thumb->buttonTypeFromPoint(e->pos());
if (e->button() == Qt::RightButton) {
showContextMenu(thumb, e->globalPos());
} else if (was == now) {
thumbButtonsCallback(thumb, now);
}
}
_pressedButtonType = AttachButtonType::None;
}
void AlbumPreview::showContextMenu(
not_null<AlbumThumbnail*> thumb,
QPoint position) {
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
const auto index = orderIndex(thumb);
if (_actionAllowed(index, AttachActionType::ToggleSpoiler)
&& _sendWay.sendImagesAsPhotos()) {
const auto spoilered = thumb->hasSpoiler();
_menu->addAction(spoilered
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now), [=] {
thumb->setSpoiler(!spoilered);
}, spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler);
}
if (_actionAllowed(index, AttachActionType::EditCover)) {
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
_thumbEditCoverRequested.fire_copy(index);
}, &st::menuIconEdit);
if (_actionAllowed(index, AttachActionType::ClearCover)) {
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
_thumbClearCoverRequested.fire_copy(index);
}, &st::menuIconCancel);
}
}
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(position);
}
}
void AlbumPreview::switchToDrag() {
_paintedAbove
= _suggestedThumb
= _draggedThumb
= base::take(_pressedThumb);
_shrinkAnimation.start(
[=] { update(); },
0.,
1.,
AlbumThumbnail::kShrinkDuration);
applyCursor(style::cur_sizeall);
update();
}
QImage AlbumPreview::generatePriceTagBackground() const {
auto wmax = 0;
auto hmax = 0;
for (auto &thumb : _thumbs) {
const auto geometry = thumb->geometry();
accumulate_max(wmax, geometry.x() + geometry.width());
accumulate_max(hmax, geometry.y() + geometry.height());
}
const auto size = QSize(wmax, hmax);
if (size.isEmpty()) {
return {};
}
const auto ratio = style::DevicePixelRatio();
const auto full = size * ratio;
const auto skip = st::historyGroupSkip;
auto result = QImage(full, QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::black);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
for (auto &thumb : _thumbs) {
const auto geometry = thumb->geometry();
if (geometry.isEmpty()) {
continue;
}
const auto w = geometry.width();
const auto h = geometry.height();
const auto wscale = (w + skip) / float64(w);
const auto hscale = (h + skip) / float64(h);
p.save();
p.translate(geometry.center());
p.scale(wscale, hscale);
p.translate(-geometry.center());
thumb->paintInAlbum(p, 0, 0, 1., 1.);
p.restore();
}
p.end();
return ::Media::Streaming::PrepareBlurredBackground(
full,
std::move(result));
}
} // namespace Ui

View File

@@ -0,0 +1,142 @@
/*
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 "ui/rp_widget.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "base/timer.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
struct PreparedFile;
struct GroupMediaLayout;
class AlbumThumbnail;
class PopupMenu;
class AlbumPreview final : public RpWidget {
public:
AlbumPreview(
QWidget *parent,
const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items,
SendFilesWay way,
Fn<bool(int, AttachActionType)> actionAllowed);
~AlbumPreview();
void setSendWay(SendFilesWay way);
[[nodiscard]] base::flat_set<int> collectSpoileredIndices();
[[nodiscard]] bool canHaveSpoiler(int index) const;
void toggleSpoilers(bool enabled);
[[nodiscard]] std::vector<int> takeOrder();
[[nodiscard]] rpl::producer<int> thumbDeleted() const {
return _thumbDeleted.events();
}
[[nodiscard]] rpl::producer<int> thumbChanged() const {
return _thumbChanged.events();
}
[[nodiscard]] rpl::producer<int> thumbModified() const {
return _thumbModified.events();
}
[[nodiscard]] rpl::producer<int> thumbEditCoverRequested() const {
return _thumbEditCoverRequested.events();
}
[[nodiscard]] rpl::producer<int> thumbClearCoverRequested() const {
return _thumbClearCoverRequested.events();
}
[[nodiscard]] rpl::producer<> orderUpdated() const {
return _orderUpdated.events();
}
[[nodiscard]] QImage generatePriceTagBackground() const;
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
int countLayoutHeight(
const std::vector<GroupMediaLayout> &layout) const;
std::vector<GroupMediaLayout> generateOrderedLayout() const;
std::vector<int> defaultOrder(int count = -1) const;
void prepareThumbs(gsl::span<Ui::PreparedFile> items);
void updateSizeAnimated(const std::vector<GroupMediaLayout> &layout);
void updateSize();
void updateFileRows();
AlbumThumbnail *thumbUnderCursor();
void deleteThumbByIndex(int index);
void changeThumbByIndex(int index);
void modifyThumbByIndex(int index);
void thumbButtonsCallback(
not_null<AlbumThumbnail*> thumb,
AttachButtonType type);
void switchToDrag();
void paintAlbum(Painter &p) const;
void paintPhotos(Painter &p, QRect clip) const;
void paintFiles(Painter &p, QRect clip) const;
void applyCursor(style::cursor cursor);
int contentLeft() const;
int contentTop() const;
AlbumThumbnail *findThumb(QPoint position) const;
not_null<AlbumThumbnail*> findClosestThumb(QPoint position) const;
void updateSuggestedDrag(QPoint position);
int orderIndex(not_null<AlbumThumbnail*> thumb) const;
void cancelDrag();
void finishDrag();
void showContextMenu(not_null<AlbumThumbnail*> thumb, QPoint position);
const style::ComposeControls &_st;
SendFilesWay _sendWay;
Fn<bool(int, AttachActionType)> _actionAllowed;
style::cursor _cursor = style::cur_default;
std::vector<int> _order;
std::vector<QSize> _itemsShownDimensions;
std::vector<std::unique_ptr<AlbumThumbnail>> _thumbs;
int _thumbsHeight = 0;
int _photosHeight = 0;
int _filesHeight = 0;
bool _hasMixedFileHeights = false;
AlbumThumbnail *_draggedThumb = nullptr;
AlbumThumbnail *_suggestedThumb = nullptr;
AlbumThumbnail *_paintedAbove = nullptr;
AlbumThumbnail *_pressedThumb = nullptr;
QPoint _draggedStartPosition;
base::Timer _dragTimer;
AttachButtonType _pressedButtonType = AttachButtonType::None;
rpl::event_stream<int> _thumbDeleted;
rpl::event_stream<int> _thumbChanged;
rpl::event_stream<int> _thumbModified;
rpl::event_stream<int> _thumbEditCoverRequested;
rpl::event_stream<int> _thumbClearCoverRequested;
rpl::event_stream<> _orderUpdated;
base::unique_qptr<PopupMenu> _menu;
mutable Animations::Simple _thumbsHeightAnimation;
mutable Animations::Simple _shrinkAnimation;
mutable Animations::Simple _finishDragAnimation;
};
} // namespace Ui

View File

@@ -0,0 +1,626 @@
/*
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 "ui/chat/attach/attach_album_thumbnail.h"
#include "core/mime_type.h" // Core::IsMimeSticker.
#include "ui/chat/attach/attach_prepare.h"
#include "ui/image/image_prepare.h"
#include "ui/text/format_values.h"
#include "ui/widgets/buttons.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/ui_utility.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "base/call_delayed.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_boxes.h"
#include <QtCore/QFileInfo>
namespace Ui {
AlbumThumbnail::AlbumThumbnail(
const style::ComposeControls &st,
const PreparedFile &file,
const GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> repaint,
Fn<void()> editCallback,
Fn<void()> deleteCallback)
: _st(st)
, _layout(layout)
, _fullPreview(file.videoCover ? file.videoCover->preview : file.preview)
, _shrinkSize(int(std::ceil(st::roundRadiusLarge / 1.4)))
, _isPhoto(file.type == PreparedFile::Type::Photo)
, _isVideo(file.type == PreparedFile::Type::Video)
, _isCompressedSticker(Core::IsMimeSticker(file.information->filemime))
, _repaint(std::move(repaint)) {
Expects(!_fullPreview.isNull());
moveToLayout(layout);
using Option = Images::Option;
const auto previewWidth = _fullPreview.width();
const auto previewHeight = _fullPreview.height();
const auto imageWidth = std::max(
previewWidth / style::DevicePixelRatio(),
st::minPhotoSize);
const auto imageHeight = std::max(
previewHeight / style::DevicePixelRatio(),
st::minPhotoSize);
_photo = PixmapFromImage(Images::Prepare(
_fullPreview,
QSize(previewWidth, previewHeight),
{
.options = Option::RoundLarge,
.outer = { imageWidth, imageHeight },
}));
const auto &layoutSt = st::attachPreviewThumbLayout;
const auto idealSize = layoutSt.thumbSize * style::DevicePixelRatio();
const auto fileThumbSize = (previewWidth > previewHeight)
? QSize(previewWidth * idealSize / previewHeight, idealSize)
: QSize(idealSize, previewHeight * idealSize / previewWidth);
_fileThumb = PixmapFromImage(Images::Prepare(
_fullPreview,
fileThumbSize,
{
.options = Option::RoundSmall,
.outer = { layoutSt.thumbSize, layoutSt.thumbSize },
}));
const auto availableFileWidth = st::sendMediaPreviewSize
- layoutSt.thumbSize
- layoutSt.thumbSkip
// Right buttons.
- st::sendBoxAlbumGroupButtonFile.width * 2
- st::sendBoxAlbumGroupEditInternalSkip * 2
- st::sendBoxAlbumGroupSkipRight;
const auto filepath = file.path;
if (filepath.isEmpty()) {
_name = "image.png";
_status = FormatImageSizeText(file.originalDimensions);
} else {
auto fileinfo = QFileInfo(filepath);
_name = fileinfo.fileName();
_status = FormatSizeText(fileinfo.size());
}
_nameWidth = st::semiboldFont->width(_name);
if (_nameWidth > availableFileWidth) {
_name = st::semiboldFont->elided(
_name,
availableFileWidth,
Qt::ElideMiddle);
_nameWidth = st::semiboldFont->width(_name);
}
_statusWidth = st::normalFont->width(_status);
_editMedia.create(parent, _st.files.buttonFile);
_deleteMedia.create(parent, _st.files.buttonFile);
const auto duration = st::historyAttach.ripple.hideDuration;
_editMedia->setClickedCallback([=] {
base::call_delayed(duration, parent, editCallback);
});
_deleteMedia->setClickedCallback(deleteCallback);
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
setSpoiler(file.spoiler);
setButtonVisible(false);
}
void AlbumThumbnail::setSpoiler(bool spoiler) {
Expects(_repaint != nullptr);
_spoiler = spoiler
? std::make_unique<SpoilerAnimation>(_repaint)
: nullptr;
_repaint();
}
bool AlbumThumbnail::hasSpoiler() const {
return _spoiler != nullptr;
}
void AlbumThumbnail::setButtonVisible(bool value) {
_editMedia->setVisible(value);
_deleteMedia->setVisible(value);
}
void AlbumThumbnail::moveButtons(int thumbTop) {
const auto top = thumbTop + st::sendBoxFileGroupSkipTop;
auto right = st::sendBoxFileGroupSkipRight + st::boxPhotoPadding.right();
_deleteMedia->moveToRight(right, top);
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
_editMedia->moveToRight(right, top);
}
void AlbumThumbnail::resetLayoutAnimation() {
_animateFromGeometry = std::nullopt;
}
void AlbumThumbnail::animateLayoutToInitial() {
_animateFromGeometry = countRealGeometry();
_suggestedMove = 0.;
_albumPosition = QPoint(0, 0);
}
void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
using namespace Images;
animateLayoutToInitial();
_layout = layout;
const auto width = _layout.geometry.width();
const auto height = _layout.geometry.height();
_albumCorners = GetCornersFromSides(_layout.sides);
const auto pixSize = GetImageScaleSizeForGeometry(
{ _fullPreview.width(), _fullPreview.height() },
{ width, height });
const auto pixWidth = pixSize.width() * style::DevicePixelRatio();
const auto pixHeight = pixSize.height() * style::DevicePixelRatio();
_albumImage = PixmapFromImage(Prepare(
_fullPreview,
QSize(pixWidth, pixHeight),
{
.options = RoundOptions(ImageRoundRadius::Large, _albumCorners),
.outer = { width, height },
}));
_albumImageBlurred = QPixmap();
}
int AlbumThumbnail::photoHeight() const {
return _photo.height() / style::DevicePixelRatio();
}
int AlbumThumbnail::fileHeight() const {
return _isCompressedSticker
? photoHeight()
: st::attachPreviewThumbLayout.thumbSize;
}
bool AlbumThumbnail::isCompressedSticker() const {
return _isCompressedSticker;
}
void AlbumThumbnail::paintInAlbum(
QPainter &p,
int left,
int top,
float64 shrinkProgress,
float64 moveProgress) {
const auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);
_lastShrinkValue = shrink;
const auto geometry = countCurrentGeometry(
moveProgress
).translated(left, top);
auto paintedTo = geometry;
const auto revealed = _spoiler ? shrinkProgress : 1.;
if (revealed > 0.) {
if (shrink > 0 || moveProgress < 1.) {
const auto size = geometry.size();
paintedTo = geometry.marginsRemoved(
{ shrink, shrink, shrink, shrink }
);
if (shrinkProgress < 1 && _albumCorners != RectPart::None) {
prepareCache(size, shrink);
p.drawImage(geometry.topLeft(), _albumCache);
} else {
drawSimpleFrame(p, paintedTo, size);
}
} else {
p.drawPixmap(geometry.topLeft(), _albumImage);
}
if (_isVideo) {
paintPlayVideo(p, geometry);
}
}
if (revealed < 1.) {
auto corners = Images::CornersMaskRef(
Images::CornersMask(ImageRoundRadius::Large));
if (!(_albumCorners & RectPart::TopLeft)) {
corners.p[0] = nullptr;
}
if (!(_albumCorners & RectPart::TopRight)) {
corners.p[1] = nullptr;
}
if (!(_albumCorners & RectPart::BottomLeft)) {
corners.p[2] = nullptr;
}
if (!(_albumCorners & RectPart::BottomRight)) {
corners.p[3] = nullptr;
}
p.setOpacity(1. - revealed);
if (_albumImageBlurred.isNull()) {
_albumImageBlurred = BlurredPreviewFromPixmap(
_albumImage,
_albumCorners);
}
p.drawPixmap(paintedTo, _albumImageBlurred);
const auto paused = On(PowerSaving::kChatSpoiler);
FillSpoilerRect(
p,
paintedTo,
corners,
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
_cornerCache);
p.setOpacity(1.);
}
_lastRectOfButtons = paintButtons(
p,
geometry,
shrinkProgress);
_lastRectOfModify = geometry;
}
void AlbumThumbnail::paintPlayVideo(QPainter &p, QRect geometry) {
const auto innerSize = st::msgFileLayout.thumbSize;
const auto inner = QRect(
geometry.x() + (geometry.width() - innerSize) / 2,
geometry.y() + (geometry.height() - innerSize) / 2,
innerSize,
innerSize);
{
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(st::msgDateImgBg);
p.drawEllipse(inner);
}
st::historyFileThumbPlay.paintInCenter(p, inner);
}
void AlbumThumbnail::prepareCache(QSize size, int shrink) {
const auto width = std::max(
_layout.geometry.width(),
_animateFromGeometry ? _animateFromGeometry->width() : 0);
const auto height = std::max(
_layout.geometry.height(),
_animateFromGeometry ? _animateFromGeometry->height() : 0);
const auto cacheSize = QSize(width, height) * style::DevicePixelRatio();
if (_albumCache.width() < cacheSize.width()
|| _albumCache.height() < cacheSize.height()) {
_albumCache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);
_albumCache.setDevicePixelRatio(style::DevicePixelRatio());
}
_albumCache.fill(Qt::transparent);
{
Painter p(&_albumCache);
const auto to = QRect(QPoint(), size).marginsRemoved(
{ shrink, shrink, shrink, shrink }
);
drawSimpleFrame(p, to, size);
}
_albumCache = Images::Round(
std::move(_albumCache),
ImageRoundRadius::Large,
_albumCorners,
QRect(QPoint(), size * style::DevicePixelRatio()));
}
void AlbumThumbnail::drawSimpleFrame(QPainter &p, QRect to, QSize size) const {
const auto fullWidth = _fullPreview.width();
const auto fullHeight = _fullPreview.height();
const auto previewSize = GetImageScaleSizeForGeometry(
{ fullWidth, fullHeight },
{ size.width(), size.height() });
const auto previewWidth = previewSize.width() * style::DevicePixelRatio();
const auto previewHeight = previewSize.height() * style::DevicePixelRatio();
const auto width = size.width() * style::DevicePixelRatio();
const auto height = size.height() * style::DevicePixelRatio();
const auto scaleWidth = to.width() / float64(width);
const auto scaleHeight = to.height() / float64(height);
const auto Round = [](float64 value) {
return int(base::SafeRound(value));
};
const auto &[from, fillBlack] = [&] {
if (previewWidth < width && previewHeight < height) {
const auto toWidth = Round(previewWidth * scaleWidth);
const auto toHeight = Round(previewHeight * scaleHeight);
return std::make_pair(
QRect(0, 0, fullWidth, fullHeight),
QMargins(
(to.width() - toWidth) / 2,
(to.height() - toHeight) / 2,
to.width() - toWidth - (to.width() - toWidth) / 2,
to.height() - toHeight - (to.height() - toHeight) / 2));
} else if (previewWidth * height > previewHeight * width) {
if (previewHeight >= height) {
const auto takeWidth = previewWidth * height / previewHeight;
const auto useWidth = fullWidth * width / takeWidth;
return std::make_pair(
QRect(
(fullWidth - useWidth) / 2,
0,
useWidth,
fullHeight),
QMargins(0, 0, 0, 0));
} else {
const auto takeWidth = previewWidth;
const auto useWidth = fullWidth * width / takeWidth;
const auto toHeight = Round(previewHeight * scaleHeight);
const auto toSkip = (to.height() - toHeight) / 2;
return std::make_pair(
QRect(
(fullWidth - useWidth) / 2,
0,
useWidth,
fullHeight),
QMargins(
0,
toSkip,
0,
to.height() - toHeight - toSkip));
}
} else {
if (previewWidth >= width) {
const auto takeHeight = previewHeight * width / previewWidth;
const auto useHeight = fullHeight * height / takeHeight;
return std::make_pair(
QRect(
0,
(fullHeight - useHeight) / 2,
fullWidth,
useHeight),
QMargins(0, 0, 0, 0));
} else {
const auto takeHeight = previewHeight;
const auto useHeight = fullHeight * height / takeHeight;
const auto toWidth = Round(previewWidth * scaleWidth);
const auto toSkip = (to.width() - toWidth) / 2;
return std::make_pair(
QRect(
0,
(fullHeight - useHeight) / 2,
fullWidth,
useHeight),
QMargins(
toSkip,
0,
to.width() - toWidth - toSkip,
0));
}
}
}();
p.drawImage(to.marginsRemoved(fillBlack), _fullPreview, from);
if (fillBlack.top() > 0) {
p.fillRect(to.x(), to.y(), to.width(), fillBlack.top(), st::imageBg);
}
if (fillBlack.bottom() > 0) {
p.fillRect(
to.x(),
to.y() + to.height() - fillBlack.bottom(),
to.width(),
fillBlack.bottom(),
st::imageBg);
}
if (fillBlack.left() > 0) {
p.fillRect(
to.x(),
to.y() + fillBlack.top(),
fillBlack.left(),
to.height() - fillBlack.top() - fillBlack.bottom(),
st::imageBg);
}
if (fillBlack.right() > 0) {
p.fillRect(
to.x() + to.width() - fillBlack.right(),
to.y() + fillBlack.top(),
fillBlack.right(),
to.height() - fillBlack.top() - fillBlack.bottom(),
st::imageBg);
}
}
void AlbumThumbnail::paintPhoto(Painter &p, int left, int top, int outerWidth) {
const auto size = _photo.size() / style::DevicePixelRatio();
if (_spoiler && _photoBlurred.isNull()) {
_photoBlurred = BlurredPreviewFromPixmap(
_photo,
RectPart::AllCorners);
}
const auto &pixmap = _spoiler ? _photoBlurred : _photo;
const auto rect = QRect(
left + (st::sendMediaPreviewSize - size.width()) / 2,
top,
pixmap.width() / pixmap.devicePixelRatio(),
pixmap.height() / pixmap.devicePixelRatio());
p.drawPixmapLeft(
left + (st::sendMediaPreviewSize - size.width()) / 2,
top,
outerWidth,
pixmap);
if (_spoiler) {
const auto paused = On(PowerSaving::kChatSpoiler);
FillSpoilerRect(
p,
rect,
Images::CornersMaskRef(
Images::CornersMask(ImageRoundRadius::Large)),
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
_cornerCache);
} else if (_isVideo) {
paintPlayVideo(p, rect);
}
const auto topLeft = QPoint{ left, top };
_lastRectOfButtons = paintButtons(
p,
QRect(left, top, st::sendMediaPreviewSize, size.height()),
0);
_lastRectOfModify = QRect(topLeft, size);
}
void AlbumThumbnail::paintFile(
Painter &p,
int left,
int top,
int outerWidth) {
if (isCompressedSticker()) {
auto spoiler = base::take(_spoiler);
paintPhoto(p, left, top, outerWidth);
_spoiler = base::take(spoiler);
return;
}
const auto &st = st::attachPreviewThumbLayout;
const auto textLeft = left + st.thumbSize + st.thumbSkip;
p.drawPixmap(left, top, _fileThumb);
p.setFont(st::semiboldFont);
p.setPen(_st.files.nameFg);
p.drawTextLeft(
textLeft,
top + st.nameTop,
outerWidth,
_name,
_nameWidth);
p.setFont(st::normalFont);
p.setPen(_st.files.statusFg);
p.drawTextLeft(
textLeft,
top + st.statusTop,
outerWidth,
_status,
_statusWidth);
_lastRectOfModify = QRect(
QPoint(left, top),
_fileThumb.size() / style::DevicePixelRatio());
}
QRect AlbumThumbnail::geometry() const {
return _layout.geometry;
}
bool AlbumThumbnail::containsPoint(QPoint position) const {
return _layout.geometry.contains(position);
}
bool AlbumThumbnail::buttonsContainPoint(QPoint position) const {
return ((_isPhoto && !_isCompressedSticker)
? _lastRectOfModify
: _lastRectOfButtons).contains(position);
}
AttachButtonType AlbumThumbnail::buttonTypeFromPoint(QPoint position) const {
if (!buttonsContainPoint(position)) {
return AttachButtonType::None;
}
return (!_lastRectOfButtons.contains(position) && !_isCompressedSticker)
? AttachButtonType::Modify
: (_buttons.vertical()
? (position.y() < _lastRectOfButtons.center().y())
: (position.x() < _lastRectOfButtons.center().x()))
? AttachButtonType::Edit
: AttachButtonType::Delete;
}
int AlbumThumbnail::distanceTo(QPoint position) const {
const auto delta = (_layout.geometry.center() - position);
return QPoint::dotProduct(delta, delta);
}
bool AlbumThumbnail::isPointAfter(QPoint position) const {
return position.x() > _layout.geometry.center().x();
}
void AlbumThumbnail::moveInAlbum(QPoint to) {
_albumPosition = to;
}
QPoint AlbumThumbnail::center() const {
auto realGeometry = _layout.geometry;
realGeometry.moveTopLeft(realGeometry.topLeft() + _albumPosition);
return realGeometry.center();
}
void AlbumThumbnail::suggestMove(float64 delta, Fn<void()> callback) {
if (_suggestedMove != delta) {
_suggestedMoveAnimation.start(
std::move(callback),
_suggestedMove,
delta,
kShrinkDuration);
_suggestedMove = delta;
}
}
QRect AlbumThumbnail::countRealGeometry() const {
const auto addLeft = int(base::SafeRound(
_suggestedMoveAnimation.value(_suggestedMove) * _lastShrinkValue));
const auto current = _layout.geometry;
const auto realTopLeft = current.topLeft()
+ _albumPosition
+ QPoint(addLeft, 0);
return { realTopLeft, current.size() };
}
QRect AlbumThumbnail::countCurrentGeometry(float64 progress) const {
const auto now = countRealGeometry();
if (_animateFromGeometry && progress < 1.) {
return {
anim::interpolate(_animateFromGeometry->x(), now.x(), progress),
anim::interpolate(_animateFromGeometry->y(), now.y(), progress),
anim::interpolate(_animateFromGeometry->width(), now.width(), progress),
anim::interpolate(_animateFromGeometry->height(), now.height(), progress)
};
}
return now;
}
void AlbumThumbnail::finishAnimations() {
_suggestedMoveAnimation.stop();
}
QRect AlbumThumbnail::paintButtons(
QPainter &p,
QRect geometry,
float64 shrinkProgress) {
const auto &skipRight = st::sendBoxAlbumGroupSkipRight;
const auto &skipTop = st::sendBoxAlbumGroupSkipTop;
const auto outerWidth = geometry.width();
const auto outerHeight = geometry.height();
if (st::sendBoxAlbumGroupSize.width() <= outerWidth) {
_buttons.setVertical(false);
} else if (st::sendBoxAlbumGroupSize.height() <= outerHeight) {
_buttons.setVertical(true);
} else {
// If the size is tiny, skip the buttons.
return QRect();
}
const auto groupWidth = _buttons.width();
const auto groupHeight = _buttons.height();
// If the width is too small,
// it would be better to display the buttons in the center.
const auto groupX = geometry.x() + ((groupWidth + skipRight * 2 > outerWidth)
? (outerWidth - groupWidth) / 2
: outerWidth - skipRight - groupWidth);
const auto groupY = geometry.y() + ((groupHeight + skipTop * 2 > outerHeight)
? (outerHeight - groupHeight) / 2
: skipTop);
const auto opacity = p.opacity();
p.setOpacity(1.0 - shrinkProgress);
_buttons.paint(p, groupX, groupY);
p.setOpacity(opacity);
return QRect(groupX, groupY, groupWidth, _buttons.height());
}
} // namespace Ui

View File

@@ -0,0 +1,124 @@
/*
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 "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/effects/animations.h"
#include "ui/grouped_layout.h"
#include "ui/round_rect.h"
#include "base/object_ptr.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
struct PreparedFile;
class IconButton;
class SpoilerAnimation;
class AlbumThumbnail final {
public:
AlbumThumbnail(
const style::ComposeControls &st,
const PreparedFile &file,
const GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> repaint,
Fn<void()> editCallback,
Fn<void()> deleteCallback);
void moveToLayout(const GroupMediaLayout &layout);
void animateLayoutToInitial();
void resetLayoutAnimation();
void setSpoiler(bool spoiler);
[[nodiscard]] bool hasSpoiler() const;
[[nodiscard]] int photoHeight() const;
[[nodiscard]] int fileHeight() const;
void paintInAlbum(
QPainter &p,
int left,
int top,
float64 shrinkProgress,
float64 moveProgress);
void paintPhoto(Painter &p, int left, int top, int outerWidth);
void paintFile(Painter &p, int left, int top, int outerWidth);
[[nodiscard]] QRect geometry() const;
[[nodiscard]] bool containsPoint(QPoint position) const;
[[nodiscard]] bool buttonsContainPoint(QPoint position) const;
[[nodiscard]] AttachButtonType buttonTypeFromPoint(
QPoint position) const;
[[nodiscard]] int distanceTo(QPoint position) const;
[[nodiscard]] bool isPointAfter(QPoint position) const;
void moveInAlbum(QPoint to);
[[nodiscard]] QPoint center() const;
void suggestMove(float64 delta, Fn<void()> callback);
void finishAnimations();
void setButtonVisible(bool value);
void moveButtons(int thumbTop);
[[nodiscard]] bool isCompressedSticker() const;
static constexpr auto kShrinkDuration = crl::time(150);
private:
QRect countRealGeometry() const;
QRect countCurrentGeometry(float64 progress) const;
void prepareCache(QSize size, int shrink);
void drawSimpleFrame(QPainter &p, QRect to, QSize size) const;
QRect paintButtons(
QPainter &p,
QRect geometry,
float64 shrinkProgress);
void paintPlayVideo(QPainter &p, QRect geometry);
const style::ComposeControls &_st;
GroupMediaLayout _layout;
std::optional<QRect> _animateFromGeometry;
const QImage _fullPreview;
const int _shrinkSize;
const bool _isPhoto;
const bool _isVideo;
QPixmap _albumImage;
QPixmap _albumImageBlurred;
QImage _albumCache;
QPoint _albumPosition;
RectParts _albumCorners = RectPart::None;
QPixmap _photo;
QPixmap _photoBlurred;
QPixmap _fileThumb;
QString _name;
QString _status;
int _nameWidth = 0;
int _statusWidth = 0;
float64 _suggestedMove = 0.;
Animations::Simple _suggestedMoveAnimation;
int _lastShrinkValue = 0;
AttachControls _buttons;
bool _isCompressedSticker = false;
std::unique_ptr<SpoilerAnimation> _spoiler;
QImage _cornerCache;
Fn<void()> _repaint;
QRect _lastRectOfModify;
QRect _lastRectOfButtons;
object_ptr<IconButton> _editMedia = { nullptr };
object_ptr<IconButton> _deleteMedia = { nullptr };
};
} // namespace Ui

View File

@@ -0,0 +1,268 @@
/*
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 "ui/chat/attach/attach_bot_downloads.h"
#include "lang/lang_keys.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
namespace Ui::BotWebView {
namespace {
class Action final : public Menu::ItemBase {
public:
Action(
not_null<RpWidget*> parent,
const DownloadsEntry &entry,
Fn<void(DownloadsAction)> callback);
bool isEnabled() const override;
not_null<QAction*> action() const override { return _dummyAction; }
void handleKeyPress(not_null<QKeyEvent*> e) override;
void refresh(const DownloadsEntry &entry);
private:
QPoint prepareRippleStartPosition() const override {
return mapFromGlobal(QCursor::pos());
}
QImage prepareRippleMask() const override {
return Ui::RippleAnimation::RectMask(size());
}
int contentHeight() const override { return _height; }
void prepare();
void paint(Painter &p);
const not_null<QAction*> _dummyAction;
const style::Menu &_st = st::defaultMenu;
DownloadsEntry _entry;
Text::String _name;
FlatLabel _progress;
IconButton _cancel;
int _textWidth = 0;
const int _height;
};
Action::Action(
not_null<RpWidget*> parent,
const DownloadsEntry &entry,
Fn<void(DownloadsAction)> callback)
: ItemBase(parent, st::defaultMenu)
, _dummyAction(new QAction(parent))
, _progress(this, st::botDownloadProgress)
, _cancel(this, st::botDownloadCancel)
, _height(st::ttlItemPadding.top()
+ _st.itemStyle.font->height
+ st::ttlItemTimerFont->height
+ st::ttlItemPadding.bottom()) {
setAcceptBoth(true);
initResizeHook(parent->sizeValue());
setClickedCallback([=] {
if (isEnabled()) {
callback(DownloadsAction::Open);
}
});
_cancel.setClickedCallback([=] {
callback(DownloadsAction::Cancel);
});
paintRequest(
) | rpl::on_next([=] {
Painter p(this);
paint(p);
}, lifetime());
widthValue() | rpl::on_next([=](int width) {
_progress.moveToLeft(
_st.itemPadding.left(),
st::ttlItemPadding.top() + _st.itemStyle.font->height,
width);
_cancel.moveToRight(
_st.itemPadding.right(),
(_height - _cancel.height()) / 2,
width);
}, lifetime());
_progress.setClickHandlerFilter([=](const auto &...) {
callback(DownloadsAction::Retry);
return false;
});
enableMouseSelecting();
refresh(entry);
}
void Action::paint(Painter &p) {
const auto selected = isSelected();
if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), _height, _st.itemBg);
}
p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
if (isEnabled()) {
paintRipple(p, 0, 0);
}
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
_name.drawLeftElided(
p,
_st.itemPadding.left(),
st::ttlItemPadding.top(),
_textWidth,
width());
_progress.setTextColorOverride(
selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
}
void Action::prepare() {
const auto filenameWidth = _name.maxWidth();
const auto progressWidth = _progress.textMaxWidth();
const auto &padding = _st.itemPadding;
const auto goodWidth = std::max(filenameWidth, progressWidth);
// Example max width: "4000 / 4000 MB"
const auto countWidth = [&](const QString &text) {
return st::ttlItemTimerFont->width(text);
};
const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
tr::now,
lt_ready,
"0000",
lt_total,
"0000",
lt_mb,
"MB"));
const auto maxStartingWidth = countWidth(
tr::lng_bot_download_starting(tr::now));
const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
tr::now,
lt_retry,
tr::lng_bot_download_retry(tr::now)));
const auto cancel = _cancel.width() + padding.right();
const auto paddings = padding.left() + padding.right() + cancel;
const auto w = std::clamp(
paddings + std::max({
goodWidth,
maxProgressWidth,
maxStartingWidth,
maxFailedWidth,
}),
_st.widthMin,
_st.widthMax);
_textWidth = w - paddings;
_progress.resizeToWidth(_textWidth);
setMinWidth(w);
update();
}
bool Action::isEnabled() const {
return _entry.total > 0 && _entry.ready == _entry.total;
}
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
if (!isSelected()) {
return;
}
const auto key = e->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
setClicked(Menu::TriggeredSource::Keyboard);
}
}
void Action::refresh(const DownloadsEntry &entry) {
_entry = entry;
const auto filename = entry.path.split('/').last();
_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
const auto progressText = (entry.total && entry.total == entry.ready)
? TextWithEntities{ FormatSizeText(entry.total) }
: entry.loading
? (entry.total
? TextWithEntities{
FormatProgressText(entry.ready, entry.total),
}
: tr::lng_bot_download_starting(tr::now, tr::marked))
: tr::lng_bot_download_failed(
tr::now,
lt_retry,
Text::Link(tr::lng_bot_download_retry(tr::now)),
tr::marked);
_progress.setMarkedText(progressText);
const auto enabled = isEnabled();
setCursor(enabled ? style::cur_pointer : style::cur_default);
_cancel.setVisible(!enabled && _entry.loading);
_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
prepare();
}
} // namespace
FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
rpl::producer<std::vector<DownloadsEntry>> content,
Fn<void(uint32, DownloadsAction)> callback) {
return [callback, moved = std::move(content)](
not_null<PopupMenu*> menu) mutable {
struct Row {
not_null<Action*> action;
uint32 id = 0;
};
struct State {
std::vector<Row> rows;
};
const auto state = menu->lifetime().make_state<State>();
std::move(
moved
) | rpl::on_next([=](
const std::vector<DownloadsEntry> &entries) {
auto found = base::flat_set<uint32>();
for (const auto &entry : entries | ranges::views::reverse) {
const auto id = entry.id;
const auto path = entry.path;
const auto i = ranges::find(state->rows, id, &Row::id);
found.emplace(id);
if (i != end(state->rows)) {
i->action->refresh(entry);
} else {
auto action = base::make_unique_q<Action>(
menu,
entry,
[=](DownloadsAction type) { callback(id, type); });
state->rows.push_back({
.action = action.get(),
.id = id,
});
menu->addAction(std::move(action));
}
}
for (auto i = begin(state->rows); i != end(state->rows);) {
if (!found.contains(i->id)) {
menu->removeAction(i - begin(state->rows));
i = state->rows.erase(i);
} else {
++i;
}
}
}, menu->lifetime());
};
}
} // namespace Ui::BotWebView

View File

@@ -0,0 +1,47 @@
/*
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
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Ui::BotWebView {
struct DownloadsProgress {
uint64 ready = 0;
uint64 total : 63 = 0;
uint64 loading : 1 = 0;
friend inline bool operator==(
const DownloadsProgress &a,
const DownloadsProgress &b) = default;
};
struct DownloadsEntry {
uint32 id = 0;
QString url;
QString path;
uint64 ready : 63 = 0;
uint64 loading : 1 = 0;
uint64 total : 63 = 0;
uint64 failed : 1 = 0;
};
enum class DownloadsAction {
Open,
Retry,
Cancel,
};
[[nodiscard]] auto FillAttachBotDownloadsSubmenu(
rpl::producer<std::vector<DownloadsEntry>> content,
Fn<void(uint32, DownloadsAction)> callback)
-> FnMut<void(not_null<PopupMenu*>)>;
} // namespace Ui::BotWebView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,268 @@
/*
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 "base/expected.h"
#include "base/object_ptr.h"
#include "base/weak_ptr.h"
#include "base/flags.h"
#include "ui/rect_part.h"
#include "ui/round_rect.h"
#include "webview/webview_common.h"
class QJsonObject;
class QJsonValue;
namespace Ui {
class FlatLabel;
class BoxContent;
class RpWidget;
class SeparatePanel;
class IconButton;
enum class LayerOption;
using LayerOptions = base::flags<LayerOption>;
} // namespace Ui
namespace Webview {
struct Available;
} // namespace Webview
namespace Ui::BotWebView {
struct DownloadsProgress;
struct DownloadsEntry;
enum class DownloadsAction;
[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);
enum class MenuButton {
None = 0x00,
OpenBot = 0x01,
RemoveFromMenu = 0x02,
RemoveFromMainMenu = 0x04,
ShareGame = 0x08,
};
inline constexpr bool is_flag_type(MenuButton) { return true; }
using MenuButtons = base::flags<MenuButton>;
using CustomMethodResult = base::expected<QByteArray, QString>;
struct CustomMethodRequest {
QString method;
QByteArray params;
Fn<void(CustomMethodResult)> callback;
};
struct SetEmojiStatusRequest {
uint64 customEmojiId = 0;
TimeId duration = 0;
Fn<void(QString)> callback;
};
struct DownloadFileRequest {
QString url;
QString name;
Fn<void(bool)> callback;
};
struct SendPreparedMessageRequest {
QString id = 0;
Fn<void(QString)> callback;
};
class Delegate {
public:
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)
-> const std::vector<DownloadsEntry> & = 0;
virtual void botDownloadsAction(uint32 id, DownloadsAction type) = 0;
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
virtual void botHandleInvoice(QString slug) = 0;
virtual void botHandleMenuButton(MenuButton button) = 0;
virtual bool botValidateExternalLink(QString uri) = 0;
virtual void botOpenIvLink(QString uri) = 0;
virtual void botSendData(QByteArray data) = 0;
virtual void botSwitchInlineQuery(
std::vector<QString> chatTypes,
QString query) = 0;
virtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual bool botStorageWrite(
QString key,
std::optional<QString> value) = 0;
[[nodiscard]] virtual std::optional<QString> botStorageRead(
QString key) = 0;
virtual void botStorageClear() = 0;
virtual void botRequestEmojiStatusAccess(
Fn<void(bool allowed)> callback) = 0;
virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
virtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;
virtual void botDownloadFile(DownloadFileRequest request) = 0;
virtual void botSendPreparedMessage(
SendPreparedMessageRequest request) = 0;
virtual void botVerifyAge(int age) = 0;
virtual void botOpenPrivacyPolicy() = 0;
virtual void botClose() = 0;
};
struct Args {
QString url;
Webview::StorageId storageId;
rpl::producer<QString> title;
object_ptr<Ui::RpWidget> titleBadge = { nullptr };
rpl::producer<QString> bottom;
not_null<Delegate*> delegate;
MenuButtons menuButtons;
bool fullscreen = false;
bool allowClipboardRead = false;
rpl::producer<DownloadsProgress> downloadsProgress;
};
class Panel final : public base::has_weak_ptr {
public:
explicit Panel(Args &&args);
~Panel();
void requestActivate();
void toggleProgress(bool shown);
void showBox(object_ptr<BoxContent> box);
void showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated);
void hideLayer(anim::type animated);
void showToast(TextWithEntities &&text);
not_null<QWidget*> toastParent() const;
void showCriticalError(const TextWithEntities &text);
void showWebviewError(
const QString &text,
const Webview::Available &information);
void updateThemeParams(const Webview::ThemeParams &params);
void hideForPayment();
void invoiceClosed(const QString &slug, const QString &status);
[[nodiscard]] rpl::lifetime &lifetime();
private:
class Button;
struct Progress;
struct WebviewWithLifetime;
bool showWebview(Args &&args, const Webview::ThemeParams &params);
bool createWebview(const Webview::ThemeParams &params);
void createWebviewBottom();
void showWebviewProgress();
void hideWebviewProgress();
void setupDownloadsProgress(
not_null<RpWidget*> button,
rpl::producer<DownloadsProgress> progress,
bool fullscreen);
void setTitle(rpl::producer<QString> title);
void sendDataMessage(const QJsonObject &args);
void switchInlineQueryMessage(const QJsonObject &args);
void processSendMessageRequest(const QJsonObject &args);
void processEmojiStatusRequest(const QJsonObject &args);
void processEmojiStatusAccessRequest();
void processStorageSaveKey(const QJsonObject &args);
void processStorageGetKey(const QJsonObject &args);
void processStorageClear(const QJsonObject &args);
void processButtonMessage(
std::unique_ptr<Button> &button,
const QJsonObject &args);
void processBackButtonMessage(const QJsonObject &args);
void processSettingsButtonMessage(const QJsonObject &args);
void processHeaderColor(const QJsonObject &args);
void processBackgroundColor(const QJsonObject &args);
void processBottomBarColor(const QJsonObject &args);
void processDownloadRequest(const QJsonObject &args);
void openTgLink(const QJsonObject &args);
void openExternalLink(const QJsonObject &args);
void openInvoice(const QJsonObject &args);
void openPopup(const QJsonObject &args);
void openScanQrPopup(const QJsonObject &args);
void openShareStory(const QJsonObject &args);
void requestWriteAccess();
void replyRequestWriteAccess(bool allowed);
void requestPhone();
void replyRequestPhone(bool shared);
void invokeCustomMethod(const QJsonObject &args);
void replyCustomMethod(QJsonValue requestId, QJsonObject response);
void requestClipboardText(const QJsonObject &args);
void setupClosingBehaviour(const QJsonObject &args);
void replyDeviceStorage(
const QJsonObject &args,
const QString &event,
QJsonObject response);
void deviceStorageFailed(const QJsonObject &args, QString error);
void secureStorageFailed(const QJsonObject &args);
void createButton(std::unique_ptr<Button> &button);
void scheduleCloseWithConfirmation();
void closeWithConfirmation();
void sendViewport();
void sendSafeArea();
void sendContentSafeArea();
void sendFullScreen();
void updateColorOverrides(const Webview::ThemeParams &params);
void overrideBodyColor(std::optional<QColor> color);
using EventData = std::variant<QString, QJsonObject>;
void postEvent(const QString &event);
void postEvent(const QString &event, EventData data);
[[nodiscard]] bool allowOpenLink() const;
[[nodiscard]] bool allowClipboardQuery() const;
[[nodiscard]] bool progressWithBackground() const;
[[nodiscard]] QRect progressRect() const;
void setupProgressGeometry();
void layoutButtons();
Webview::StorageId _storageId;
const not_null<Delegate*> _delegate;
bool _closeNeedConfirmation = false;
bool _hasSettingsButton = false;
MenuButtons _menuButtons = {};
std::unique_ptr<SeparatePanel> _widget;
std::unique_ptr<WebviewWithLifetime> _webview;
std::unique_ptr<RpWidget> _webviewBottom;
QPointer<FlatLabel> _webviewBottomLabel;
rpl::variable<QString> _bottomText;
QPointer<RpWidget> _webviewParent;
std::unique_ptr<RpWidget> _bottomButtonsBg;
std::unique_ptr<Button> _mainButton;
std::unique_ptr<Button> _secondaryButton;
RectPart _secondaryPosition = RectPart::Left;
rpl::variable<int> _footerHeight = 0;
std::unique_ptr<Progress> _progress;
rpl::event_stream<> _themeUpdateForced;
std::optional<QColor> _bottomBarColor;
rpl::lifetime _headerColorLifetime;
rpl::lifetime _bodyColorLifetime;
rpl::lifetime _bottomBarColorLifetime;
rpl::event_stream<> _downloadsUpdated;
rpl::variable<bool> _fullscreen = false;
bool _layerShown : 1 = false;
bool _webviewProgress : 1 = false;
bool _themeUpdateScheduled : 1 = false;
bool _hiddenForPayment : 1 = false;
bool _closeWithConfirmationScheduled : 1 = false;
bool _allowClipboardRead : 1 = false;
bool _inBlockingRequest : 1 = false;
bool _headerColorReceived : 1 = false;
bool _bodyColorReceived : 1 = false;
bool _bottomColorReceived : 1 = false;
};
[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);
} // namespace Ui::BotWebView

View File

@@ -0,0 +1,117 @@
/*
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 "ui/chat/attach/attach_controls.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
AttachControls::AttachControls()
: _rect(st::sendBoxAlbumGroupRadius, st::roundedBg) {
}
void AttachControls::paint(QPainter &p, int x, int y) {
const auto groupWidth = width();
const auto groupHeight = height();
const auto full = (_type == Type::Full);
QRect groupRect(x, y, groupWidth, groupHeight);
_rect.paint(p, groupRect);
if (full) {
const auto groupHalfWidth = groupWidth / 2;
const auto groupHalfHeight = groupHeight / 2;
const auto editRect = _vertical
? QRect(x, y, groupWidth, groupHalfHeight)
: QRect(x, y, groupHalfWidth, groupHeight);
st::sendBoxAlbumGroupButtonMediaEdit.paintInCenter(p, editRect);
const auto deleteRect = _vertical
? QRect(x, y + groupHalfHeight, groupWidth, groupHalfHeight)
: QRect(x + groupHalfWidth, y, groupHalfWidth, groupHeight);
st::sendBoxAlbumGroupButtonMediaDelete.paintInCenter(p, deleteRect);
} else if (_type == Type::EditOnly) {
st::sendBoxAlbumButtonMediaEdit.paintInCenter(p, groupRect);
}
}
int AttachControls::width() const {
return (_type == Type::Full)
? (_vertical
? st::sendBoxAlbumGroupSizeVertical.width()
: st::sendBoxAlbumGroupSize.width())
: (_type == Type::EditOnly)
? st::sendBoxAlbumSmallGroupSize.width()
: 0;
}
int AttachControls::height() const {
return (_type == Type::Full)
? (_vertical
? st::sendBoxAlbumGroupSizeVertical.height()
: st::sendBoxAlbumGroupSize.height())
: (_type == Type::EditOnly)
? st::sendBoxAlbumSmallGroupSize.height()
: 0;
}
AttachControls::Type AttachControls::type() const {
return _type;
}
bool AttachControls::vertical() const {
return _vertical;
}
void AttachControls::setType(Type type) {
if (_type != type) {
_type = type;
}
}
void AttachControls::setVertical(bool vertical) {
_vertical = vertical;
}
AttachControlsWidget::AttachControlsWidget(
not_null<RpWidget*> parent,
AttachControls::Type type)
: RpWidget(parent)
, _edit(base::make_unique_q<AbstractButton>(this))
, _delete(base::make_unique_q<AbstractButton>(this)) {
_controls.setType(type);
const auto w = _controls.width();
resize(w, _controls.height());
if (type == AttachControls::Type::Full) {
_edit->resize(w / 2, _controls.height());
_delete->resize(w / 2, _controls.height());
_edit->moveToLeft(0, 0, w);
_delete->moveToRight(0, 0, w);
} else if (type == AttachControls::Type::EditOnly) {
_edit->resize(w, _controls.height());
_edit->moveToLeft(0, 0, w);
}
paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(this);
_controls.paint(p, 0, 0);
}, lifetime());
}
rpl::producer<> AttachControlsWidget::editRequests() const {
return _edit->clicks() | rpl::to_empty;
}
rpl::producer<> AttachControlsWidget::deleteRequests() const {
return _delete->clicks() | rpl::to_empty;
}
} // namespace Ui

View File

@@ -0,0 +1,58 @@
/*
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 "ui/abstract_button.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
namespace Ui {
class AttachControls final {
public:
enum class Type {
Full,
EditOnly,
None,
};
AttachControls();
void paint(QPainter &p, int x, int y);
void setType(Type type);
void setVertical(bool vertical);
[[nodiscard]] int width() const;
[[nodiscard]] int height() const;
[[nodiscard]] Type type() const;
[[nodiscard]] bool vertical() const;
private:
RoundRect _rect;
Type _type = Type::Full;
bool _vertical = false;
};
class AttachControlsWidget final : public RpWidget {
public:
AttachControlsWidget(
not_null<RpWidget*> parent,
AttachControls::Type type = AttachControls::Type::Full);
[[nodiscard]] rpl::producer<> editRequests() const;
[[nodiscard]] rpl::producer<> deleteRequests() const;
private:
const base::unique_qptr<AbstractButton> _edit;
const base::unique_qptr<AbstractButton> _delete;
AttachControls _controls;
};
} // namespace Ui

View File

@@ -0,0 +1,30 @@
/*
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 "ui/chat/attach/attach_extensions.h"
#include <QtCore/QMimeDatabase>
#include <QtGui/QImageReader>
namespace Ui {
const QStringList &ImageExtensions() {
static const auto result = [] {
const auto formats = QImageReader::supportedImageFormats();
return formats | ranges::views::transform([](const auto &format) {
return '.' + format.toLower();
}) | ranges::views::filter([](const auto &format) {
const auto mimes = QMimeDatabase().mimeTypesForFileName(
u"test"_q + format);
return !mimes.isEmpty()
&& mimes.front().name().startsWith(u"image/"_q);
}) | ranges::to<QStringList>;
}();
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,14 @@
/*
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
namespace Ui {
[[nodiscard]] const QStringList &ImageExtensions();
} // namespace Ui

View File

@@ -0,0 +1,115 @@
/*
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 "ui/chat/attach/attach_item_single_file_preview.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "history/history_item.h"
#include "history/view/media/history_view_document.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/text/format_song_name.h"
#include "ui/text/format_values.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
namespace Ui {
namespace {
AttachControls::Type CheckControlsType(
not_null<HistoryItem*> item,
AttachControls::Type type) {
const auto media = item->media();
Assert(media != nullptr);
return media->allowsEditMedia()
? type
: AttachControls::Type::None;
}
} // namespace
ItemSingleFilePreview::ItemSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
not_null<HistoryItem*> item,
AttachControls::Type type)
: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type)) {
const auto media = item->media();
Assert(media != nullptr);
const auto document = media->document();
Assert(document != nullptr);
_documentMedia = document->createMediaView();
_documentMedia->thumbnailWanted(item->fullId());
rpl::single(rpl::empty) | rpl::then(
document->session().downloaderTaskFinished()
) | rpl::on_next([=] {
if (_documentMedia->thumbnail()) {
_lifetimeDownload.destroy();
}
preparePreview(document);
}, _lifetimeDownload);
}
void ItemSingleFilePreview::preparePreview(not_null<DocumentData*> document) {
AbstractSingleFilePreview::Data data;
const auto preview = _documentMedia->thumbnail()
? _documentMedia->thumbnail()->original()
: QImage();
prepareThumbFor(data, preview);
data.fileIsImage = document->isImage();
data.fileIsAudio = document->isAudioFile() || document->isVoiceMessage();
if (data.fileIsImage) {
data.name = document->filename();
// data.statusText = FormatImageSizeText(preview.size()
// / preview.devicePixelRatio());
} else if (data.fileIsAudio) {
auto filename = document->filename();
auto songTitle = QString();
auto songPerformer = QString();
if (const auto song = document->song()) {
songTitle = song->title;
songPerformer = song->performer;
if (document->isSongWithCover()) {
const auto size = QSize(
st::attachPreviewLayout.thumbSize,
st::attachPreviewLayout.thumbSize);
auto thumb = QPixmap(size);
thumb.fill(Qt::transparent);
Painter p(&thumb);
HistoryView::DrawThumbnailAsSongCover(
p,
st::songCoverOverlayFg,
_documentMedia,
QRect(QPoint(), size));
data.fileThumb = std::move(thumb);
}
} else if (document->isVoiceMessage()) {
songTitle = tr::lng_media_audio(tr::now);
}
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
.string();
} else {
data.name = document->filename();
}
data.statusText = FormatSizeText(document->size);
setData(data);
}
} // namespace Ui

View File

@@ -0,0 +1,41 @@
/*
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 "ui/chat/attach/attach_abstract_single_file_preview.h"
class HistoryItem;
class DocumentData;
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Ui {
struct PreparedFile;
class IconButton;
class ItemSingleFilePreview final : public AbstractSingleFilePreview {
public:
ItemSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
not_null<HistoryItem*> item,
AttachControls::Type type);
private:
void preparePreview(not_null<DocumentData*> document);
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
rpl::lifetime _lifetimeDownload;
};
} // namespace Ui

View File

@@ -0,0 +1,245 @@
/*
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 "ui/chat/attach/attach_item_single_media_preview.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
#include "history/history_item.h"
#include "history/view/media/history_view_document.h"
#include "main/main_session.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_loader_local.h"
#include "media/streaming/media_streaming_player.h"
#include "styles/style_boxes.h"
namespace Ui {
namespace {
using namespace ::Media::Streaming;
} // namespace
ItemSingleMediaPreview::ItemSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
not_null<HistoryItem*> item,
AttachControls::Type type)
: AbstractSingleMediaPreview(parent, st, type, [=](AttachActionType type) {
if (type == AttachActionType::EditCover) {
return _isVideoFile;
}
return true;
})
, _gifPaused(std::move(gifPaused))
, _isVideoFile(item->media()->document()
&& item->media()->document()->isVideoFile())
, _fullId(item->fullId()) {
const auto media = item->media();
Assert(media != nullptr);
Main::Session *session = nullptr;
if (const auto photo = media->photo()) {
_photoMedia = photo->createMediaView();
_photoMedia->wanted(Data::PhotoSize::Large, item->fullId());
session = &photo->session();
} else if (const auto document = media->document()) {
_documentMedia = document->createMediaView();
_documentMedia->thumbnailWanted(item->fullId());
session = &document->session();
if (document->isAnimation() || document->isVideoFile()) {
setAnimated(true);
prepareStreamedPreview();
}
} else {
Unexpected("Photo or document should be set.");
}
struct ThumbInfo {
bool loaded = false;
Image *image = nullptr;
};
const auto computeThumbInfo = [=]() -> ThumbInfo {
using Size = Data::PhotoSize;
if (_documentMedia) {
return { true, _documentMedia->thumbnail() };
} else if (const auto large = _photoMedia->image(Size::Large)) {
return { true, large };
} else if (const auto thumbnail = _photoMedia->image(
Size::Thumbnail)) {
return { false, thumbnail };
} else if (const auto small = _photoMedia->image(Size::Small)) {
return { false, small };
} else {
return { false, _photoMedia->thumbnailInline() };
}
};
rpl::single(rpl::empty) | rpl::then(
session->downloaderTaskFinished()
) | rpl::on_next([=] {
const auto computed = computeThumbInfo();
if (!computed.image) {
if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {
const auto size = _documentMedia->owner()->dimensions.scaled(
st::sendMediaPreviewSize,
st::confirmMaxHeight,
Qt::KeepAspectRatio);
if (!size.isEmpty()) {
auto empty = QImage(
size,
QImage::Format_ARGB32_Premultiplied);
empty.fill(Qt::black);
preparePreview(empty);
}
_lifetimeDownload.destroy();
}
return;
} else if (computed.loaded) {
_lifetimeDownload.destroy();
}
preparePreview(computed.image->original());
}, _lifetimeDownload);
}
void ItemSingleMediaPreview::prepareStreamedPreview() {
if (_streamed || !_documentMedia) {
return;
}
const auto document = _documentMedia
? _documentMedia->owner().get()
: nullptr;
if (document && document->isAnimation()) {
setupStreamedPreview(
document->owner().streaming().sharedDocument(
document,
_fullId));
}
}
void ItemSingleMediaPreview::setupStreamedPreview(
std::shared_ptr<Document> shared) {
if (!shared) {
return;
}
_streamed = std::make_unique<Instance>(
std::move(shared),
[=] { update(); });
_streamed->lockPlayer();
_streamed->player().updates(
) | rpl::on_next_error([=](Update &&update) {
handleStreamingUpdate(std::move(update));
}, [=](Error &&error) {
handleStreamingError(std::move(error));
}, _streamed->lifetime());
if (_streamed->ready()) {
streamingReady(base::duplicate(_streamed->info()));
}
checkStreamedIsStarted();
}
void ItemSingleMediaPreview::handleStreamingUpdate(Update &&update) {
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [](PreloadedVideo) {
}, [&](UpdateVideo) {
this->update();
}, [](PreloadedAudio) {
}, [](UpdateAudio) {
}, [](WaitingForData) {
}, [](SpeedEstimate) {
}, [](MutedByOther) {
}, [](Finished) {
});
}
void ItemSingleMediaPreview::handleStreamingError(Error &&error) {
}
void ItemSingleMediaPreview::streamingReady(Information &&info) {
}
void ItemSingleMediaPreview::checkStreamedIsStarted() {
if (!_streamed) {
return;
} else if (_streamed->paused()) {
_streamed->resume();
}
if (!_streamed->active() && !_streamed->failed()) {
startStreamedPlayer();
}
}
void ItemSingleMediaPreview::startStreamedPlayer() {
auto options = ::Media::Streaming::PlaybackOptions();
options.audioId = _documentMedia
? AudioMsgId(_documentMedia->owner(), _fullId)
: AudioMsgId();
options.waitForMarkAsShown = true;
//if (!_streamed->withSound) {
options.mode = ::Media::Streaming::Mode::Video;
options.loop = true;
//}
_streamed->play(options);
}
bool ItemSingleMediaPreview::supportsSpoilers() const {
return false; // We are not allowed to change existing spoiler setting.
}
bool ItemSingleMediaPreview::drawBackground() const {
return true; // A sticker can't be here.
}
bool ItemSingleMediaPreview::tryPaintAnimation(QPainter &p) {
checkStreamedIsStarted();
if (_streamed
&& _streamed->player().ready()
&& !_streamed->player().videoSize().isEmpty()) {
const auto s = QSize(previewWidth(), previewHeight());
const auto paused = _gifPaused();
auto request = ::Media::Streaming::FrameRequest();
request.outer = s * style::DevicePixelRatio();
request.resize = s * style::DevicePixelRatio();
p.drawImage(
QRect(
previewLeft(),
previewTop(),
previewWidth(),
previewHeight()),
_streamed->frame(request));
if (!paused) {
_streamed->markFrameShown();
}
return true;
}
return false;
}
bool ItemSingleMediaPreview::isAnimatedPreviewReady() const {
return _streamed != nullptr;
}
auto ItemSingleMediaPreview::sharedPhotoMedia() const
-> std::shared_ptr<::Data::PhotoMedia> {
return _photoMedia;
}
} // namespace Ui

View File

@@ -0,0 +1,72 @@
/*
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 "ui/chat/attach/attach_abstract_single_media_preview.h"
namespace Data {
class DocumentMedia;
class PhotoMedia;
} // namespace Data
namespace Media {
namespace Streaming {
class Instance;
class Document;
struct Update;
enum class Error;
struct Information;
} // namespace Streaming
} // namespace Media
class HistoryItem;
namespace Ui {
class ItemSingleMediaPreview final : public AbstractSingleMediaPreview {
public:
ItemSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
not_null<HistoryItem*> item,
AttachControls::Type type);
std::shared_ptr<::Data::PhotoMedia> sharedPhotoMedia() const;
protected:
bool supportsSpoilers() const override;
bool drawBackground() const override;
bool tryPaintAnimation(QPainter &p) override;
bool isAnimatedPreviewReady() const override;
private:
void prepareStreamedPreview();
void checkStreamedIsStarted();
void setupStreamedPreview(
std::shared_ptr<::Media::Streaming::Document> shared);
void handleStreamingUpdate(::Media::Streaming::Update &&update);
void handleStreamingError(::Media::Streaming::Error &&error);
void streamingReady(::Media::Streaming::Information &&info);
void startStreamedPlayer();
const Fn<bool()> _gifPaused;
const bool _isVideoFile;
const FullMsgId _fullId;
std::shared_ptr<::Data::PhotoMedia> _photoMedia;
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
std::unique_ptr<::Media::Streaming::Instance> _streamed;
rpl::lifetime _lifetimeDownload;
};
} // namespace Ui

View File

@@ -0,0 +1,390 @@
/*
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 "ui/chat/attach/attach_prepare.h"
#include "ui/rp_widget.h"
#include "ui/widgets/popup_menu.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/image/image_prepare.h"
#include "ui/ui_utility.h"
#include "core/mime_type.h"
namespace Ui {
namespace {
constexpr auto kMaxAlbumCount = 10;
} // namespace
PreparedFile::PreparedFile(const QString &path) : path(path) {
}
PreparedFile::PreparedFile(PreparedFile &&other) = default;
PreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;
PreparedFile::~PreparedFile() = default;
bool PreparedFile::canBeInAlbumType(AlbumType album) const {
return CanBeInAlbumType(type, album);
}
bool PreparedFile::isSticker() const {
Expects(information != nullptr);
return (type == PreparedFile::Type::Photo)
&& Core::IsMimeSticker(information->filemime);
}
bool PreparedFile::isVideoFile() const {
Expects(information != nullptr);
using Video = Ui::PreparedFileInformation::Video;
return (type == PreparedFile::Type::Video)
&& v::is<Video>(information->media)
&& !v::get<Video>(information->media).isGifv;
}
bool PreparedFile::isGifv() const {
Expects(information != nullptr);
using Video = Ui::PreparedFileInformation::Video;
return (type == PreparedFile::Type::Video)
&& v::is<Video>(information->media)
&& v::get<Video>(information->media).isGifv;
}
AlbumType PreparedFile::albumType(bool sendImagesAsPhotos) const {
switch (type) {
case Type::Photo:
return sendImagesAsPhotos ? AlbumType::PhotoVideo : AlbumType::File;
case Type::Video:
return AlbumType::PhotoVideo;
case Type::Music:
return AlbumType::Music;
case Type::File:
return AlbumType::File;
case Type::None:
return AlbumType::None;
}
Unexpected("PreparedFile::type in PreparedFile::albumType().");
}
bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) {
Expects(album != AlbumType::None);
using Type = PreparedFile::Type;
switch (album) {
case AlbumType::PhotoVideo:
return (type == Type::Photo) || (type == Type::Video);
case AlbumType::Music:
return (type == Type::Music);
case AlbumType::File:
return (type == Type::Photo) || (type == Type::File);
}
Unexpected("AlbumType in CanBeInAlbumType.");
}
bool InsertTextOnImageCancel(const QString &text) {
return !text.isEmpty() && !text.startsWith(u"data:image"_q);
}
PreparedList PreparedList::Reordered(
PreparedList &&list,
std::vector<int> order) {
Expects(list.error == PreparedList::Error::None);
Expects(list.files.size() == order.size());
auto result = PreparedList(list.error, list.errorData);
result.files.reserve(list.files.size());
for (auto index : order) {
result.files.push_back(std::move(list.files[index]));
}
return result;
}
void PreparedList::mergeToEnd(PreparedList &&other, bool cutToAlbumSize) {
if (error != Error::None) {
return;
}
if (other.error != Error::None) {
error = other.error;
errorData = other.errorData;
return;
}
files.reserve(std::min(
size_t(cutToAlbumSize ? kMaxAlbumCount : INT_MAX),
files.size() + other.files.size()));
for (auto &file : other.files) {
if (cutToAlbumSize && files.size() == kMaxAlbumCount) {
break;
}
files.push_back(std::move(file));
}
}
bool PreparedList::canBeSentInSlowmode() const {
return canBeSentInSlowmodeWith(PreparedList());
}
bool PreparedList::canBeSentInSlowmodeWith(const PreparedList &other) const {
if (!filesToProcess.empty() || !other.filesToProcess.empty()) {
return false;
} else if (files.size() + other.files.size() < 2) {
return true;
} else if (files.size() + other.files.size() > kMaxAlbumCount) {
return false;
}
using Type = PreparedFile::Type;
auto &&all = ranges::views::concat(files, other.files);
const auto has = [&](Type type) {
return ranges::contains(all, type, &PreparedFile::type);
};
const auto hasNonGrouping = has(Type::None);
const auto hasPhotos = has(Type::Photo);
const auto hasFiles = has(Type::File);
const auto hasVideos = has(Type::Video);
const auto hasMusic = has(Type::Music);
// File-s and Video-s never can be grouped.
// Music-s can be grouped only with themselves.
if (hasNonGrouping) {
return false;
} else if (hasFiles) {
return !hasMusic && !hasVideos;
} else if (hasVideos) {
return !hasMusic && !hasFiles;
} else if (hasMusic) {
return !hasVideos && !hasFiles && !hasPhotos;
}
return !hasNonGrouping && (!hasFiles || !hasVideos);
}
bool PreparedList::canAddCaption(bool sendingAlbum, bool compress) const {
if (!filesToProcess.empty()
|| files.empty()
|| files.size() > kMaxAlbumCount) {
return false;
}
if (files.size() == 1) {
Assert(files.front().information != nullptr);
const auto isSticker = (!compress
&& Core::IsMimeSticker(files.front().information->filemime))
|| files.front().path.endsWith(u".tgs"_q, Qt::CaseInsensitive);
return !isSticker;
} else if (!sendingAlbum) {
return false;
}
const auto hasFiles = ranges::contains(
files,
PreparedFile::Type::File,
&PreparedFile::type);
const auto hasMusic = ranges::contains(
files,
PreparedFile::Type::Music,
&PreparedFile::type);
const auto hasNotGrouped = ranges::contains(
files,
PreparedFile::Type::None,
&PreparedFile::type);
return !hasFiles && !hasMusic && !hasNotGrouped;
}
bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {
if (!canAddCaption(sendingAlbum, compress)) {
return false;
} else if (files.size() != 1) {
return true;
}
const auto &file = files.front();
return (file.type == PreparedFile::Type::Video)
|| (file.type == PreparedFile::Type::Photo && compress);
}
bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const {
return canMoveCaption(sendingAlbum, compress);
}
bool PreparedList::hasGroupOption(bool slowmode) const {
if (slowmode || files.size() < 2) {
return false;
}
using Type = PreparedFile::Type;
auto lastType = Type::None;
for (const auto &file : files) {
if ((file.type == lastType)
|| (file.type == Type::Video && lastType == Type::Photo)
|| (file.type == Type::Photo && lastType == Type::Video)
|| (file.type == Type::File && lastType == Type::Photo)
|| (file.type == Type::Photo && lastType == Type::File)) {
if (lastType != Type::None) {
return true;
}
}
lastType = file.type;
}
return false;
}
bool PreparedList::hasSendImagesAsPhotosOption(bool slowmode) const {
using Type = PreparedFile::Type;
return slowmode
? ((files.size() == 1) && (files.front().type == Type::Photo))
: ranges::contains(files, Type::Photo, &PreparedFile::type);
}
bool PreparedList::canHaveEditorHintLabel() const {
for (const auto &file : files) {
if ((file.type == PreparedFile::Type::Photo)
&& !Core::IsMimeSticker(file.information->filemime)) {
return true;
}
}
return false;
}
bool PreparedList::hasSticker() const {
return ranges::any_of(files, &PreparedFile::isSticker);
}
bool PreparedList::hasSpoilerMenu(bool compress) const {
const auto allAreVideo = !ranges::any_of(files, [](const auto &f) {
using Type = Ui::PreparedFile::Type;
return (f.type != Type::Video);
});
const auto allAreMedia = !ranges::any_of(files, [](const auto &f) {
using Type = Ui::PreparedFile::Type;
return (f.type != Type::Photo) && (f.type != Type::Video);
});
return allAreVideo || (allAreMedia && compress);
}
std::shared_ptr<PreparedBundle> PrepareFilesBundle(
std::vector<PreparedGroup> groups,
SendFilesWay way,
TextWithTags caption,
bool ctrlShiftEnter) {
auto totalCount = 0;
for (const auto &group : groups) {
totalCount += group.list.files.size();
}
const auto sendComment = !caption.text.isEmpty()
&& (groups.size() != 1 || !groups.front().sentWithCaption());
return std::make_shared<PreparedBundle>(PreparedBundle{
.groups = std::move(groups),
.way = way,
.caption = std::move(caption),
.totalCount = totalCount + (sendComment ? 1 : 0),
.sendComment = sendComment,
.ctrlShiftEnter = ctrlShiftEnter,
});
}
int MaxAlbumItems() {
return kMaxAlbumCount;
}
bool ValidateThumbDimensions(int width, int height) {
return (width > 0)
&& (height > 0)
&& (width <= 20 * height)
&& (height <= 20 * width);
}
std::vector<PreparedGroup> DivideByGroups(
PreparedList &&list,
SendFilesWay way,
bool slowmode) {
const auto sendImagesAsPhotos = way.sendImagesAsPhotos();
const auto groupFiles = way.groupFiles() || slowmode;
auto group = Ui::PreparedList();
using Type = Ui::PreparedFile::Type;
auto groupType = AlbumType::None;
auto result = std::vector<PreparedGroup>();
auto pushGroup = [&] {
const auto type = (group.files.size() > 1)
? groupType
: AlbumType::None;
result.push_back(PreparedGroup{
.list = base::take(group),
.type = type,
});
};
for (auto i = 0; i != list.files.size(); ++i) {
auto &file = list.files[i];
const auto fileGroupType = (file.type == Type::Music)
? (groupFiles ? AlbumType::Music : AlbumType::None)
: (file.type == Type::Video)
? (groupFiles ? AlbumType::PhotoVideo : AlbumType::None)
: (file.type == Type::Photo)
? ((groupFiles && sendImagesAsPhotos)
? AlbumType::PhotoVideo
: (groupFiles && !sendImagesAsPhotos)
? AlbumType::File
: AlbumType::None)
: (file.type == Type::File)
? (groupFiles ? AlbumType::File : AlbumType::None)
: AlbumType::None;
if ((!group.files.empty() && groupType != fileGroupType)
|| ((groupType != AlbumType::None)
&& (group.files.size() == Ui::MaxAlbumItems()))) {
pushGroup();
}
group.files.push_back(std::move(file));
groupType = fileGroupType;
}
if (!group.files.empty()) {
pushGroup();
}
return result;
}
QPixmap PrepareSongCoverForThumbnail(QImage image, int size) {
const auto scaledSize = image.size().scaled(
size,
size,
Qt::KeepAspectRatioByExpanding);
using Option = Images::Option;
const auto ratio = style::DevicePixelRatio();
return PixmapFromImage(Images::Prepare(
std::move(image),
scaledSize * ratio,
{
.colored = &st::songCoverOverlayFg,
.options = Option::RoundCircle,
.outer = { size, size },
}));
}
QPixmap BlurredPreviewFromPixmap(QPixmap pixmap, RectParts corners) {
const auto image = pixmap.toImage();
const auto skip = st::roundRadiusLarge * image.devicePixelRatio();
auto small = image.copy(
skip,
skip,
image.width() - 2 * skip,
image.height() - 2 * skip
).scaled(
40,
40,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
using namespace Images;
return PixmapFromImage(Prepare(
Blur(std::move(small), true),
image.size(),
{ .options = RoundOptions(ImageRoundRadius::Large, corners) }));
}
} // namespace Ui

View File

@@ -0,0 +1,180 @@
/*
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 "editor/photo_editor_common.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/rect_part.h"
#include <QtCore/QSemaphore>
#include <deque>
namespace Ui {
class RpWidget;
class SendFilesWay;
struct PreparedFileInformation {
struct Image {
QImage data;
QByteArray bytes;
QByteArray format;
bool animated = false;
Editor::PhotoModifications modifications;
};
struct Song {
crl::time duration = -1;
QString title;
QString performer;
QImage cover;
};
struct Video {
bool isGifv = false;
bool isWebmSticker = false;
bool supportsStreaming = false;
crl::time duration = -1;
QImage thumbnail;
};
QString filemime;
std::variant<v::null_t, Image, Song, Video> media;
};
enum class AlbumType {
None,
PhotoVideo,
Music,
File,
};
struct PreparedFile {
// File-s can be grouped if 'groupFiles'.
// File-s + Photo-s can be grouped if 'groupFiles && !sendImagesAsPhotos'.
// Photo-s can be grouped if 'groupFiles'.
// Photo-s + Video-s can be grouped if 'groupFiles && sendImagesAsPhotos'.
// Video-s can be grouped if 'groupFiles'.
// Music-s can be grouped if 'groupFiles'.
enum class Type {
None,
Photo,
Video,
Music,
File,
};
PreparedFile(const QString &path);
PreparedFile(PreparedFile &&other);
PreparedFile &operator=(PreparedFile &&other);
~PreparedFile();
[[nodiscard]] bool canBeInAlbumType(AlbumType album) const;
[[nodiscard]] AlbumType albumType(bool sendImagesAsPhotos) const;
[[nodiscard]] bool isSticker() const;
[[nodiscard]] bool isVideoFile() const;
[[nodiscard]] bool isGifv() const;
QString path;
QByteArray content;
int64 size = 0;
std::unique_ptr<PreparedFileInformation> information;
std::unique_ptr<PreparedFile> videoCover;
QImage preview;
QSize shownDimensions;
QSize originalDimensions;
Type type = Type::File;
bool spoiler = false;
};
[[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);
[[nodiscard]] bool InsertTextOnImageCancel(const QString &text);
struct PreparedList {
enum class Error {
None,
NonLocalUrl,
Directory,
EmptyFile,
TooLargeFile,
};
PreparedList() = default;
PreparedList(Error error, QString errorData)
: error(error)
, errorData(errorData) {
}
PreparedList(PreparedList &&other) = default;
PreparedList &operator=(PreparedList &&other) = default;
[[nodiscard]] static PreparedList Reordered(
PreparedList &&list,
std::vector<int> order);
void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false);
[[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const;
[[nodiscard]] bool canMoveCaption(
bool sendingAlbum,
bool compress) const;
[[nodiscard]] bool canChangePrice(
bool sendingAlbum,
bool compress) const;
[[nodiscard]] bool canBeSentInSlowmode() const;
[[nodiscard]] bool canBeSentInSlowmodeWith(
const PreparedList &other) const;
[[nodiscard]] bool hasGroupOption(bool slowmode) const;
[[nodiscard]] bool hasSendImagesAsPhotosOption(bool slowmode) const;
[[nodiscard]] bool canHaveEditorHintLabel() const;
[[nodiscard]] bool hasSticker() const;
[[nodiscard]] bool hasSpoilerMenu(bool compress) const;
Error error = Error::None;
QString errorData;
std::vector<PreparedFile> files;
std::deque<PreparedFile> filesToProcess;
std::optional<bool> overrideSendImagesAsPhotos;
};
struct PreparedGroup {
PreparedList list;
AlbumType type = AlbumType::None;
[[nodiscard]] bool sentWithCaption() const {
return (list.files.size() == 1)
|| (type == AlbumType::PhotoVideo);
}
};
[[nodiscard]] std::vector<PreparedGroup> DivideByGroups(
PreparedList &&list,
SendFilesWay way,
bool slowmode);
struct PreparedBundle {
std::vector<PreparedGroup> groups;
SendFilesWay way;
TextWithTags caption;
int totalCount = 0;
bool sendComment = false;
bool ctrlShiftEnter = false;
};
[[nodiscard]] std::shared_ptr<PreparedBundle> PrepareFilesBundle(
std::vector<PreparedGroup> groups,
SendFilesWay way,
TextWithTags caption,
bool ctrlShiftEnter);
[[nodiscard]] int MaxAlbumItems();
[[nodiscard]] bool ValidateThumbDimensions(int width, int height);
[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);
[[nodiscard]] QPixmap BlurredPreviewFromPixmap(
QPixmap pixmap,
RectParts corners);
} // namespace Ui

View File

@@ -0,0 +1,69 @@
/*
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 "ui/chat/attach/attach_send_files_way.h"
namespace Ui {
void SendFilesWay::setSendImagesAsPhotos(bool value) {
if (value) {
_flags |= Flag::SendImagesAsPhotos;
} else {
if (hasCompressedStickers()) {
setGroupFiles(false);
}
_flags &= ~Flag::SendImagesAsPhotos;
}
}
void SendFilesWay::setGroupFiles(bool value) {
if (value) {
_flags |= Flag::GroupFiles;
if (hasCompressedStickers()) {
setSendImagesAsPhotos(true);
}
} else {
_flags &= ~Flag::GroupFiles;
}
}
void SendFilesWay::setHasCompressedStickers(bool value) {
if (value) {
_flags |= Flag::HasCompressedStickers;
} else {
_flags &= ~Flag::HasCompressedStickers;
}
}
//enum class SendFilesWay { // Old way. Serialize should be compatible.
// Album,
// Photos,
// Files,
//};
int32 SendFilesWay::serialize() const {
auto result = (sendImagesAsPhotos() && groupFiles())
? int32(0)
: sendImagesAsPhotos()
? int32(1)
: groupFiles()
? int32(3)
: int32(2);
return result;
}
std::optional<SendFilesWay> SendFilesWay::FromSerialized(int32 value) {
if (value < 0 || value > 3) {
return std::nullopt;
}
auto result = SendFilesWay();
result.setGroupFiles((value == 0) || (value == 3));
result.setSendImagesAsPhotos((value == 0) || (value == 1));
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,80 @@
/*
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 "base/flags.h"
namespace Ui {
enum class AttachActionType {
ToggleSpoiler,
EditCover,
ClearCover,
};
enum class AttachButtonType {
Edit,
Delete,
Modify,
None,
};
class SendFilesWay final {
public:
[[nodiscard]] bool groupFiles() const {
return (_flags & Flag::GroupFiles) != 0;
}
[[nodiscard]] bool sendImagesAsPhotos() const {
return (_flags & Flag::SendImagesAsPhotos) != 0;
}
void setGroupFiles(bool value);
void setSendImagesAsPhotos(bool value);
void setHasCompressedStickers(bool value);
[[nodiscard]] inline bool operator<(const SendFilesWay &other) const {
return _flags < other._flags;
}
[[nodiscard]] inline bool operator>(const SendFilesWay &other) const {
return other < *this;
}
[[nodiscard]] inline bool operator<=(const SendFilesWay &other) const {
return !(other < *this);
}
[[nodiscard]] inline bool operator>=(const SendFilesWay &other) const {
return !(*this < other);
}
[[nodiscard]] inline bool operator==(const SendFilesWay &other) const {
return _flags == other._flags;
}
[[nodiscard]] inline bool operator!=(const SendFilesWay &other) const {
return !(*this == other);
}
[[nodiscard]] int32 serialize() const;
[[nodiscard]] static std::optional<SendFilesWay> FromSerialized(
int32 value);
private:
[[nodiscard]] bool hasCompressedStickers() const {
return (_flags & Flag::HasCompressedStickers) != 0;
}
enum class Flag : uchar {
GroupFiles = (1 << 0),
SendImagesAsPhotos = (1 << 1),
HasCompressedStickers = (1 << 2),
Default = GroupFiles | SendImagesAsPhotos,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
base::flags<Flag> _flags = Flag::Default;
};
} // namespace Ui

View File

@@ -0,0 +1,80 @@
/*
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 "ui/chat/attach/attach_single_file_preview.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/text/format_song_name.h"
#include "ui/text/format_values.h"
#include "ui/ui_utility.h"
#include "core/mime_type.h"
#include "styles/style_chat.h"
#include <QtCore/QFileInfo>
namespace Ui {
SingleFilePreview::SingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
const PreparedFile &file,
AttachControls::Type type)
: AbstractSingleFilePreview(parent, st, type) {
preparePreview(file);
}
void SingleFilePreview::preparePreview(const PreparedFile &file) {
AbstractSingleFilePreview::Data data;
auto preview = QImage();
if (const auto image = std::get_if<PreparedFileInformation::Image>(
&file.information->media)) {
preview = image->data;
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
&file.information->media)) {
preview = video->thumbnail;
}
prepareThumbFor(data, preview);
const auto filepath = file.path;
if (filepath.isEmpty()) {
auto filename = "image.png";
data.name = filename;
data.statusText = FormatImageSizeText(file.originalDimensions);
data.fileIsImage = true;
} else {
auto fileinfo = QFileInfo(filepath);
auto filename = fileinfo.fileName();
data.fileIsImage = Core::FileIsImage(
filename,
Core::MimeTypeForFile(fileinfo).name());
auto songTitle = QString();
auto songPerformer = QString();
if (file.information) {
if (const auto song = std::get_if<PreparedFileInformation::Song>(
&file.information->media)) {
songTitle = song->title;
songPerformer = song->performer;
data.fileIsAudio = true;
if (auto cover = song->cover; !cover.isNull()) {
data.fileThumb = Ui::PrepareSongCoverForThumbnail(
cover,
st::attachPreviewLayout.thumbSize);
}
}
}
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
.string();
data.statusText = FormatSizeText(fileinfo.size());
}
setData(data);
}
} // namespace Ui

View File

@@ -0,0 +1,29 @@
/*
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 "ui/chat/attach/attach_abstract_single_file_preview.h"
namespace Ui {
struct PreparedFile;
class SingleFilePreview final : public AbstractSingleFilePreview {
public:
SingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
const PreparedFile &file,
AttachControls::Type type = AttachControls::Type::Full);
private:
void preparePreview(const PreparedFile &file);
};
} // namespace Ui

View File

@@ -0,0 +1,168 @@
/*
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 "ui/chat/attach/attach_single_media_preview.h"
#include "editor/photo_editor_common.h"
#include "ui/chat/attach/attach_prepare.h"
#include "core/mime_type.h"
#include "lottie/lottie_single_player.h"
namespace Ui {
SingleMediaPreview *SingleMediaPreview::Create(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
const PreparedFile &file,
Fn<bool(AttachActionType)> actionAllowed,
AttachControls::Type type) {
auto preview = QImage();
auto animated = false;
auto animationPreview = false;
auto hasModifications = false;
if (const auto image = std::get_if<PreparedFileInformation::Image>(
&file.information->media)) {
preview = Editor::ImageModified(image->data, image->modifications);
animated = animationPreview = image->animated;
hasModifications = !image->modifications.empty();
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
&file.information->media)) {
preview = file.videoCover
? file.videoCover->preview
: video->thumbnail;
animated = true;
animationPreview = video->isGifv;
}
if (preview.isNull()) {
return nullptr;
} else if (!animated
&& !ValidateThumbDimensions(preview.width(), preview.height())
&& !hasModifications) {
return nullptr;
}
return CreateChild<SingleMediaPreview>(
parent,
st,
std::move(gifPaused),
preview,
animated,
Core::IsMimeSticker(file.information->filemime),
file.spoiler,
animationPreview ? file.path : QString(),
type,
std::move(actionAllowed));
}
SingleMediaPreview::SingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
QImage preview,
bool animated,
bool sticker,
bool spoiler,
const QString &animatedPreviewPath,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed)
: AbstractSingleMediaPreview(parent, st, type, std::move(actionAllowed))
, _gifPaused(std::move(gifPaused))
, _sticker(sticker) {
Expects(!preview.isNull());
setAnimated(animated);
preparePreview(preview);
prepareAnimatedPreview(animatedPreviewPath, animated);
setSpoiler(spoiler);
}
bool SingleMediaPreview::supportsSpoilers() const {
return !_sticker || sendWay().sendImagesAsPhotos();
}
bool SingleMediaPreview::drawBackground() const {
return !_sticker;
}
bool SingleMediaPreview::tryPaintAnimation(QPainter &p) {
if (_gifPreview && _gifPreview->started()) {
const auto paused = _gifPaused();
const auto frame = _gifPreview->current({
.frame = QSize(previewWidth(), previewHeight()),
}, paused ? 0 : crl::now());
p.drawImage(previewLeft(), previewTop(), frame);
return true;
} else if (_lottiePreview && _lottiePreview->ready()) {
const auto frame = _lottiePreview->frame();
const auto size = frame.size() / style::DevicePixelRatio();
p.drawImage(
QRect(
previewLeft() + (previewWidth() - size.width()) / 2,
(previewHeight() - size.height()) / 2,
size.width(),
size.height()),
frame);
_lottiePreview->markFrameShown();
return true;
}
return false;
}
bool SingleMediaPreview::isAnimatedPreviewReady() const {
return _gifPreview || _lottiePreview;
}
void SingleMediaPreview::prepareAnimatedPreview(
const QString &animatedPreviewPath,
bool animated) {
if (_sticker && animated) {
const auto box = QSize(previewWidth(), previewHeight())
* style::DevicePixelRatio();
_lottiePreview = std::make_unique<Lottie::SinglePlayer>(
Lottie::ReadContent(QByteArray(), animatedPreviewPath),
Lottie::FrameRequest{ box });
_lottiePreview->updates(
) | rpl::on_next([=] {
update();
}, lifetime());
} else if (!animatedPreviewPath.isEmpty()) {
auto callback = [=](Media::Clip::Notification notification) {
clipCallback(notification);
};
_gifPreview = Media::Clip::MakeReader(
animatedPreviewPath,
std::move(callback));
}
}
void SingleMediaPreview::clipCallback(
Media::Clip::Notification notification) {
using namespace Media::Clip;
switch (notification) {
case Notification::Reinit: {
if (_gifPreview && _gifPreview->state() == State::Error) {
_gifPreview.setBad();
}
if (_gifPreview && _gifPreview->ready() && !_gifPreview->started()) {
_gifPreview->start({
.frame = QSize(previewWidth(), previewHeight()),
});
}
update();
} break;
case Notification::Repaint: {
if (_gifPreview && !_gifPreview->currentDisplayed()) {
update();
}
} break;
}
}
} // namespace Ui

View File

@@ -0,0 +1,62 @@
/*
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 "ui/chat/attach/attach_abstract_single_media_preview.h"
#include "media/clip/media_clip_reader.h"
namespace Lottie {
class SinglePlayer;
} // namespace Lottie
namespace Ui {
struct PreparedFile;
class SingleMediaPreview final : public AbstractSingleMediaPreview {
public:
static SingleMediaPreview *Create(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
const PreparedFile &file,
Fn<bool(AttachActionType)> actionAllowed,
AttachControls::Type type = AttachControls::Type::Full);
SingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
QImage preview,
bool animated,
bool sticker,
bool spoiler,
const QString &animatedPreviewPath,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed);
protected:
bool supportsSpoilers() const override;
bool drawBackground() const override;
bool tryPaintAnimation(QPainter &p) override;
bool isAnimatedPreviewReady() const override;
private:
void prepareAnimatedPreview(
const QString &animatedPreviewPath,
bool animated);
void clipCallback(Media::Clip::Notification notification);
const Fn<bool()> _gifPaused;
const bool _sticker = false;
Media::Clip::ReaderPointer _gifPreview;
std::unique_ptr<Lottie::SinglePlayer> _lottiePreview;
};
} // namespace Ui

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,668 @@
/*
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 "ui/cached_round_corners.h"
#include "ui/chat/message_bubble.h"
#include "ui/chat/chat_style_radius.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/style/style_core_palette.h"
#include "layout/layout_selection.h"
#include "styles/style_basic.h"
enum class ImageRoundRadius;
namespace style {
struct TwoIconButton;
struct ScrollArea;
} // namespace style
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Ui {
class ChatTheme;
class ChatStyle;
struct BubblePattern;
inline constexpr auto kColorPatternsCount = Text::kMaxQuoteOutlines;
inline constexpr auto kColorIndexCount = uint8(1 << 6);
inline constexpr auto kSimpleColorIndexCount = uint8(7);
inline constexpr auto kDefaultBgOpacity = 0.12;
inline constexpr auto kDefaultOutline1Opacity = 0.9;
inline constexpr auto kDefaultOutline2Opacity = 0.3;
inline constexpr auto kDefaultOutline3Opacity = 0.6;
inline constexpr auto kDefaultOutlineOpacitySecond = 0.5;
struct MessageStyle {
CornersPixmaps msgBgCornersSmall;
CornersPixmaps msgBgCornersLarge;
style::color msgBg;
style::color msgShadow;
style::color msgServiceFg;
style::color msgDateFg;
style::color msgFileThumbLinkFg;
style::color msgFileBg;
style::color msgReplyBarColor;
style::color msgWaveformActive;
style::color msgWaveformInactive;
style::color historyTextFg;
style::color historyFileNameFg;
style::color historyFileRadialFg;
style::color mediaFg;
style::TextPalette textPalette;
style::TextPalette semiboldPalette;
style::TextPalette fwdTextPalette;
style::TextPalette replyTextPalette;
style::icon tailLeft = { Qt::Uninitialized };
style::icon tailRight = { Qt::Uninitialized };
style::icon historyRepliesIcon = { Qt::Uninitialized };
style::icon historyViewsIcon = { Qt::Uninitialized };
style::icon historyPinIcon = { Qt::Uninitialized };
style::icon historySentIcon = { Qt::Uninitialized };
style::icon historyReceivedIcon = { Qt::Uninitialized };
style::icon historyPsaIcon = { Qt::Uninitialized };
style::icon historyCommentsOpen = { Qt::Uninitialized };
style::icon historyComments = { Qt::Uninitialized };
style::icon historyCallArrow = { Qt::Uninitialized };
style::icon historyCallArrowMissed = { Qt::Uninitialized };
style::icon historyCallIcon = { Qt::Uninitialized };
style::icon historyCallCameraIcon = { Qt::Uninitialized };
style::icon historyCallGroupIcon = { Qt::Uninitialized };
style::icon historyFilePlay = { Qt::Uninitialized };
style::icon historyFileWaiting = { Qt::Uninitialized };
style::icon historyFileDownload = { Qt::Uninitialized };
style::icon historyFileCancel = { Qt::Uninitialized };
style::icon historyFilePause = { Qt::Uninitialized };
style::icon historyFileImage = { Qt::Uninitialized };
style::icon historyFileDocument = { Qt::Uninitialized };
style::icon historyAudioDownload = { Qt::Uninitialized };
style::icon historyAudioCancel = { Qt::Uninitialized };
style::icon historyQuizTimer = { Qt::Uninitialized };
style::icon historyQuizExplain = { Qt::Uninitialized };
style::icon historyPollChosen = { Qt::Uninitialized };
style::icon historyPollChoiceRight = { Qt::Uninitialized };
style::icon historyTranscribeIcon = { Qt::Uninitialized };
style::icon historyTranscribeLock = { Qt::Uninitialized };
style::icon historyTranscribeHide = { Qt::Uninitialized };
style::icon historyVoiceMessageTTL = { Qt::Uninitialized };
style::icon liveLocationLongIcon = { Qt::Uninitialized };
std::array<
std::unique_ptr<Text::QuotePaintCache>,
kColorPatternsCount> quoteCache;
std::array<
std::unique_ptr<Text::QuotePaintCache>,
kColorPatternsCount> replyCache;
std::unique_ptr<Text::QuotePaintCache> preCache;
};
struct MessageImageStyle {
CornersPixmaps msgDateImgBgCorners;
CornersPixmaps msgServiceBgCornersSmall;
CornersPixmaps msgServiceBgCornersLarge;
CornersPixmaps msgShadowCornersSmall;
CornersPixmaps msgShadowCornersLarge;
style::color msgServiceBg;
style::color msgDateImgBg;
style::color msgShadow;
style::color historyFileThumbRadialFg;
style::icon historyFileThumbPlay = { Qt::Uninitialized };
style::icon historyFileThumbWaiting = { Qt::Uninitialized };
style::icon historyFileThumbDownload = { Qt::Uninitialized };
style::icon historyFileThumbCancel = { Qt::Uninitialized };
style::icon historyFileThumbPause = { Qt::Uninitialized };
style::icon historyVideoDownload = { Qt::Uninitialized };
style::icon historyVideoCancel = { Qt::Uninitialized };
style::icon historyVideoMessageMute = { Qt::Uninitialized };
style::icon historyVideoMessageTtlIcon = { Qt::Uninitialized };
style::icon historyPageEnlarge = { Qt::Uninitialized };
};
struct ColorCollectible {
uint64 collectibleId = 0;
uint64 giftEmojiId = 0;
uint64 backgroundEmojiId = 0;
QColor accentColor;
std::vector<QColor> strip;
QColor darkAccentColor;
std::vector<QColor> darkStrip;
friend inline bool operator==(
const ColorCollectible &,
const ColorCollectible &) = default;
};
struct ColorCollectiblePtrCompare {
bool operator()(
const std::weak_ptr<ColorCollectible> &a,
const std::weak_ptr<ColorCollectible> &b) const {
return a.owner_before(b);
}
};
struct ReactionPaintInfo {
QPoint position;
QPoint effectOffset;
Fn<QRect(QPainter&)> effectPaint;
};
struct BackgroundEmojiCache {
QColor color;
std::array<QImage, 3> frames;
};
struct BackgroundEmojiData {
std::unique_ptr<Text::CustomEmoji> emoji;
QImage firstFrameMask;
base::flat_map<int, BackgroundEmojiCache> caches;
base::flat_map<
std::weak_ptr<ColorCollectible>,
BackgroundEmojiCache,
ColorCollectiblePtrCompare> collectibleCaches;
std::unique_ptr<Text::CustomEmoji> gift;
QImage firstGiftFrame;
[[nodiscard]] static int CacheIndex(
bool selected,
bool outbg,
bool inbubble,
uint8 colorIndexPlusOne);
};
struct ChatPaintHighlight {
float64 opacity = 0.;
float64 collapsion = 0.;
TextSelection range;
int todoItemId = 0;
};
struct ChatPaintContext {
not_null<const ChatStyle*> st;
const BubblePattern *bubblesPattern = nullptr;
ReactionPaintInfo *reactionInfo = nullptr;
QRect viewport;
QRect clip;
TextSelection selection;
ChatPaintHighlight highlight;
QPainterPath *highlightPathCache = nullptr;
mutable QRect highlightInterpolateTo;
crl::time now = 0;
Ui::Controls::SwipeContextData gestureHorizontal;
void translate(int x, int y) {
viewport.translate(x, y);
clip.translate(x, y);
highlightInterpolateTo.translate(x, y);
}
void translate(QPoint point) {
translate(point.x(), point.y());
}
[[nodiscard]] bool selected() const {
return (selection == FullSelection);
}
[[nodiscard]] not_null<const MessageStyle*> messageStyle() const;
[[nodiscard]] not_null<const MessageImageStyle*> imageStyle() const;
[[nodiscard]] not_null<Text::QuotePaintCache*> quoteCache(
const std::shared_ptr<ColorCollectible> &colorCollectible,
uint8 colorIndex) const;
[[nodiscard]] ChatPaintContext translated(int x, int y) const {
auto result = *this;
result.translate(x, y);
return result;
}
[[nodiscard]] ChatPaintContext translated(QPoint point) const {
return translated(point.x(), point.y());
}
[[nodiscard]] ChatPaintContext withSelection(
TextSelection selection) const {
auto result = *this;
result.selection = selection;
return result;
}
[[nodiscard]] auto computeHighlightCache() const
-> std::optional<Ui::Text::HighlightInfoRequest> {
if (highlight.range.empty() || highlight.collapsion <= 0.) {
return {};
}
return Ui::Text::HighlightInfoRequest{
.range = highlight.range,
.interpolateTo = highlightInterpolateTo,
.interpolateProgress = (1. - highlight.collapsion),
.outPath = highlightPathCache,
};
};
// This is supported only in unwrapped media and text messages for now.
enum class SkipDrawingParts {
None,
Content,
Surrounding,
Bubble,
};
SkipDrawingParts skipDrawingParts = SkipDrawingParts::None;
bool outbg = false;
bool paused = false;
};
struct ChatPaintContextArgs {
not_null<ChatTheme*> theme;
QRect clip;
QPoint visibleAreaPositionGlobal;
int visibleAreaTop = 0;
int visibleAreaWidth = 0;
};
[[nodiscard]] int HistoryServiceMsgRadius();
[[nodiscard]] int HistoryServiceMsgInvertedRadius();
[[nodiscard]] int HistoryServiceMsgInvertedShrink();
struct ColorIndexData {
std::array<uint32, kColorPatternsCount> light = {};
std::array<uint32, kColorPatternsCount> dark = {};
friend inline bool operator==(
const ColorIndexData&,
const ColorIndexData&) = default;
};
struct ColorIndicesCompressed {
std::shared_ptr<std::array<ColorIndexData, kColorIndexCount>> colors;
};
[[nodiscard]] int ColorPatternIndex(
const ColorIndicesCompressed &indices,
uint8 colorIndex,
bool dark);
struct ColorIndexValues {
std::array<QColor, kColorPatternsCount> outlines;
QColor name;
QColor bg;
};
[[nodiscard]] ColorIndexValues SimpleColorIndexValues(
QColor color,
int patternIndex);
class ChatStyle final : public style::palette {
public:
explicit ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices);
explicit ChatStyle(not_null<const style::palette*> isolated);
ChatStyle(const ChatStyle &other) = delete;
ChatStyle &operator=(const ChatStyle &other) = delete;
~ChatStyle();
void apply(not_null<ChatTheme*> theme);
void applyCustomPalette(const style::palette *palette);
void applyAdjustedServiceBg(QColor serviceBg);
[[nodiscard]] bool dark() const {
return _dark;
}
[[nodiscard]] std::span<Text::SpecialColor> highlightColors() const;
[[nodiscard]] rpl::producer<> paletteChanged() const {
return _paletteChanged.events();
}
template <typename Type>
[[nodiscard]] Type value(const Type &original) const {
auto my = Type();
make(my, original);
return my;
}
template <typename Type>
[[nodiscard]] const Type &value(
rpl::lifetime &parentLifetime,
const Type &original) const {
const auto my = parentLifetime.make_state<Type>();
make(*my, original);
return *my;
}
[[nodiscard]] const CornersPixmaps &serviceBgCornersNormal() const;
[[nodiscard]] const CornersPixmaps &serviceBgCornersInverted() const;
[[nodiscard]] const MessageStyle &messageStyle(
bool outbg,
bool selected) const;
[[nodiscard]] const MessageImageStyle &imageStyle(bool selected) const;
[[nodiscard]] int colorPatternIndex(uint8 colorIndex) const;
[[nodiscard]] int collectiblePatternIndex(
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] ColorIndexValues computeColorIndexValues(
bool selected,
uint8 colorIndex) const;
[[nodiscard]] auto serviceQuoteCache(bool twoColored) const
-> not_null<Text::QuotePaintCache*>;
[[nodiscard]] auto serviceReplyCache(bool twoColored) const
-> not_null<Text::QuotePaintCache*>;
[[nodiscard]] const ColorIndexValues &coloredValues(
bool selected,
uint8 colorIndex) const;
[[nodiscard]] QColor collectibleNameColor(
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] not_null<Text::QuotePaintCache*> coloredQuoteCache(
bool selected,
uint8 colorIndex) const;
[[nodiscard]] not_null<Text::QuotePaintCache*> coloredReplyCache(
bool selected,
uint8 colorIndex) const;
[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleQuoteCache(
bool selected,
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleReplyCache(
bool selected,
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] const style::TextPalette &coloredTextPalette(
bool selected,
uint8 colorIndex) const;
[[nodiscard]] const style::TextPalette &collectibleTextPalette(
bool selected,
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] not_null<BackgroundEmojiData*> backgroundEmojiData(
uint64 emojiId,
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersSmall() const;
[[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersLarge() const;
[[nodiscard]] const CornersPixmaps &msgSelectOverlayCorners(
CachedCornerRadius radius) const;
[[nodiscard]] const style::TextPalette &historyPsaForwardPalette() const {
return _historyPsaForwardPalette;
}
[[nodiscard]] const style::TextPalette &imgReplyTextPalette() const {
return _imgReplyTextPalette;
}
[[nodiscard]] const style::TextPalette &serviceTextPalette() const {
return _serviceTextPalette;
}
[[nodiscard]] const style::TextPalette &priceTagTextPalette() const {
return _priceTagTextPalette;
}
[[nodiscard]] const style::icon &historyRepliesInvertedIcon() const {
return _historyRepliesInvertedIcon;
}
[[nodiscard]] const style::icon &historyViewsInvertedIcon() const {
return _historyViewsInvertedIcon;
}
[[nodiscard]] const style::icon &historyViewsSendingIcon() const {
return _historyViewsSendingIcon;
}
[[nodiscard]] const style::icon &historyViewsSendingInvertedIcon() const {
return _historyViewsSendingInvertedIcon;
}
[[nodiscard]] const style::icon &historyPinInvertedIcon() const {
return _historyPinInvertedIcon;
}
[[nodiscard]] const style::icon &historySendingIcon() const {
return _historySendingIcon;
}
[[nodiscard]] const style::icon &historySendingInvertedIcon() const {
return _historySendingInvertedIcon;
}
[[nodiscard]] const style::icon &historySentInvertedIcon() const {
return _historySentInvertedIcon;
}
[[nodiscard]] const style::icon &historyReceivedInvertedIcon() const {
return _historyReceivedInvertedIcon;
}
[[nodiscard]] const style::icon &msgBotKbUrlIcon() const {
return _msgBotKbUrlIcon;
}
[[nodiscard]] const style::icon &msgBotKbPaymentIcon() const {
return _msgBotKbPaymentIcon;
}
[[nodiscard]] const style::icon &msgBotKbSwitchPmIcon() const {
return _msgBotKbSwitchPmIcon;
}
[[nodiscard]] const style::icon &msgBotKbWebviewIcon() const {
return _msgBotKbWebviewIcon;
}
[[nodiscard]] const style::icon &msgBotKbCopyIcon() const {
return _msgBotKbCopyIcon;
}
[[nodiscard]] const style::icon &historyFastCommentsIcon() const {
return _historyFastCommentsIcon;
}
[[nodiscard]] const style::icon &historyFastShareIcon() const {
return _historyFastShareIcon;
}
[[nodiscard]] const style::icon &historyFastTranscribeIcon() const {
return _historyFastTranscribeIcon;
}
[[nodiscard]] const style::icon &historyFastTranscribeLock() const {
return _historyFastTranscribeLock;
}
[[nodiscard]] const style::icon &historyGoToOriginalIcon() const {
return _historyGoToOriginalIcon;
}
[[nodiscard]] const style::icon &historyFastCloseIcon() const {
return _historyFastCloseIcon;
}
[[nodiscard]] const style::icon &historyFastMoreIcon() const {
return _historyFastMoreIcon;
}
[[nodiscard]] const style::icon &historyMapPoint() const {
return _historyMapPoint;
}
[[nodiscard]] const style::icon &historyMapPointInner() const {
return _historyMapPointInner;
}
[[nodiscard]] const style::icon &youtubeIcon() const {
return _youtubeIcon;
}
[[nodiscard]] const style::icon &videoIcon() const {
return _videoIcon;
}
[[nodiscard]] const style::icon &historyPollChoiceRight() const {
return _historyPollChoiceRight;
}
[[nodiscard]] const style::icon &historyPollChoiceWrong() const {
return _historyPollChoiceWrong;
}
private:
using ColoredQuotePaintCaches = std::array<
std::unique_ptr<Text::QuotePaintCache>,
kColorIndexCount * 2>;
struct ColoredPalette {
ColoredPalette();
ColoredPalette(const ColoredPalette &other);
ColoredPalette &operator=(const ColoredPalette &other);
std::optional<style::owned_color> linkFg;
style::TextPalette data;
};
struct CollectibleColors {
std::unique_ptr<Text::QuotePaintCache> quote;
std::unique_ptr<Text::QuotePaintCache> quoteSelected;
std::unique_ptr<Text::QuotePaintCache> reply;
std::unique_ptr<Text::QuotePaintCache> replySelected;
ColoredPalette palette;
ColoredPalette paletteSelected;
};
void assignPalette(not_null<const style::palette*> palette);
void clearColorIndexCaches();
void updateDarkValue();
[[nodiscard]] not_null<Text::QuotePaintCache*> coloredCache(
ColoredQuotePaintCaches &caches,
bool selected,
uint8 colorIndex) const;
[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleCache(
std::unique_ptr<Text::QuotePaintCache> &cache,
const std::shared_ptr<ColorCollectible> &collectible) const;
[[nodiscard]] CollectibleColors &resolveCollectibleCaches(
const std::shared_ptr<ColorCollectible> &collectible) const;
void make(style::color &my, const style::color &original) const;
void make(style::icon &my, const style::icon &original) const;
void make(
style::TextPalette &my,
const style::TextPalette &original) const;
void make(
style::TwoIconButton &my,
const style::TwoIconButton &original) const;
void make(
style::ScrollArea &my,
const style::ScrollArea &original) const;
[[nodiscard]] MessageStyle &messageStyleRaw(
bool outbg,
bool selected) const;
[[nodiscard]] MessageStyle &messageIn();
[[nodiscard]] MessageStyle &messageInSelected();
[[nodiscard]] MessageStyle &messageOut();
[[nodiscard]] MessageStyle &messageOutSelected();
[[nodiscard]] MessageImageStyle &imageStyleRaw(bool selected) const;
[[nodiscard]] MessageImageStyle &image();
[[nodiscard]] MessageImageStyle &imageSelected();
template <typename Type>
void make(
Type MessageStyle::*my,
const Type &originalIn,
const Type &originalInSelected,
const Type &originalOut,
const Type &originalOutSelected);
template <typename Type>
void make(
Type MessageImageStyle::*my,
const Type &original,
const Type &originalSelected);
mutable CornersPixmaps _serviceBgCornersNormal;
mutable CornersPixmaps _serviceBgCornersInverted;
mutable std::array<MessageStyle, 4> _messageStyles;
mutable std::array<MessageImageStyle, 2> _imageStyles;
mutable CornersPixmaps _msgBotKbOverBgAddCornersSmall;
mutable CornersPixmaps _msgBotKbOverBgAddCornersLarge;
mutable CornersPixmaps _msgSelectOverlayCorners[
int(CachedCornerRadius::kCount)];
mutable std::vector<Text::SpecialColor> _highlightColors;
mutable std::array<
std::unique_ptr<Text::QuotePaintCache>,
2> _serviceQuoteCache;
mutable std::array<
std::unique_ptr<Text::QuotePaintCache>,
2> _serviceReplyCache;
mutable std::array<
std::optional<ColorIndexValues>,
2 * kColorIndexCount> _coloredValues;
mutable ColoredQuotePaintCaches _coloredQuoteCaches;
mutable ColoredQuotePaintCaches _coloredReplyCaches;
mutable std::array<
ColoredPalette,
2 * kColorIndexCount> _coloredTextPalettes;
mutable base::flat_map<
std::weak_ptr<ColorCollectible>,
CollectibleColors,
ColorCollectiblePtrCompare> _collectibleCaches;
mutable base::flat_map<uint64, BackgroundEmojiData> _backgroundEmojis;
style::TextPalette _historyPsaForwardPalette;
style::TextPalette _imgReplyTextPalette;
style::TextPalette _serviceTextPalette;
style::TextPalette _priceTagTextPalette;
style::icon _historyRepliesInvertedIcon = { Qt::Uninitialized };
style::icon _historyViewsInvertedIcon = { Qt::Uninitialized };
style::icon _historyViewsSendingIcon = { Qt::Uninitialized };
style::icon _historyViewsSendingInvertedIcon = { Qt::Uninitialized };
style::icon _historyPinInvertedIcon = { Qt::Uninitialized };
style::icon _historySendingIcon = { Qt::Uninitialized };
style::icon _historySendingInvertedIcon = { Qt::Uninitialized };
style::icon _historySentInvertedIcon = { Qt::Uninitialized };
style::icon _historyReceivedInvertedIcon = { Qt::Uninitialized };
style::icon _msgBotKbUrlIcon = { Qt::Uninitialized };
style::icon _msgBotKbPaymentIcon = { Qt::Uninitialized };
style::icon _msgBotKbSwitchPmIcon = { Qt::Uninitialized };
style::icon _msgBotKbWebviewIcon = { Qt::Uninitialized };
style::icon _msgBotKbCopyIcon = { Qt::Uninitialized };
style::icon _historyFastCommentsIcon = { Qt::Uninitialized };
style::icon _historyFastShareIcon = { Qt::Uninitialized };
style::icon _historyFastMoreIcon = { Qt::Uninitialized };
style::icon _historyFastTranscribeIcon = { Qt::Uninitialized };
style::icon _historyFastTranscribeLock = { Qt::Uninitialized };
style::icon _historyGoToOriginalIcon = { Qt::Uninitialized };
style::icon _historyFastCloseIcon = { Qt::Uninitialized };
style::icon _historyMapPoint = { Qt::Uninitialized };
style::icon _historyMapPointInner = { Qt::Uninitialized };
style::icon _youtubeIcon = { Qt::Uninitialized };
style::icon _videoIcon = { Qt::Uninitialized };
style::icon _historyPollChoiceRight = { Qt::Uninitialized };
style::icon _historyPollChoiceWrong = { Qt::Uninitialized };
ColorIndicesCompressed _colorIndices;
bool _dark = false;
rpl::event_stream<> _paletteChanged;
rpl::lifetime _defaultPaletteChangeLifetime;
rpl::lifetime _colorIndicesLifetime;
};
[[nodiscard]] uint8 DecideColorIndex(uint64 id);
[[nodiscard]] uint8 ColorIndexToPaletteIndex(uint8 colorIndex);
[[nodiscard]] QColor FromNameFg(
not_null<const ChatStyle*> st,
bool selected,
uint8 colorIndex,
const std::shared_ptr<Ui::ColorCollectible> &colorCollectible);
[[nodiscard]] inline QColor FromNameFg(
const ChatPaintContext &context,
uint8 colorIndex,
const std::shared_ptr<Ui::ColorCollectible> &colorCollectible) {
return context.outbg
? context.messageStyle()->msgServiceFg->c
: FromNameFg(
context.st,
context.selected(),
colorIndex,
colorCollectible);
}
void FillComplexOverlayRect(
QPainter &p,
QRect rect,
const style::color &color,
const CornersPixmaps &corners);
void FillComplexEllipse(
QPainter &p,
not_null<const ChatStyle*> st,
QRect rect);
} // namespace Ui

View File

@@ -0,0 +1,61 @@
/*
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 "ui/chat/chat_style_radius.h"
#include "ui/chat/chat_style.h"
#include "base/options.h"
#include "ui/chat/chat_theme.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_chat.h"
namespace Ui {
namespace {
base::options::toggle UseSmallMsgBubbleRadius({
.id = kOptionUseSmallMsgBubbleRadius,
.name = "Use small message bubble radius",
.description = "Makes most message bubbles square-ish.",
.restartRequired = true,
});
} // namespace
const char kOptionUseSmallMsgBubbleRadius[] = "use-small-msg-bubble-radius";
int BubbleRadiusSmall() {
return st::bubbleRadiusSmall;
}
int BubbleRadiusLarge() {
static const auto result = [] {
if (UseSmallMsgBubbleRadius.value()) {
return st::bubbleRadiusSmall;
} else {
return st::bubbleRadiusLarge;
}
}();
return result;
}
int MsgFileThumbRadiusSmall() {
return st::msgFileThumbRadiusSmall;
}
int MsgFileThumbRadiusLarge() {
static const auto result = [] {
if (UseSmallMsgBubbleRadius.value()) {
return st::msgFileThumbRadiusSmall;
} else {
return st::msgFileThumbRadiusLarge;
}
}();
return result;
}
}

View File

@@ -0,0 +1,20 @@
/*
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
namespace Ui {
[[nodiscard]] int BubbleRadiusSmall();
[[nodiscard]] int BubbleRadiusLarge();
[[nodiscard]] int MsgFileThumbRadiusSmall();
[[nodiscard]] int MsgFileThumbRadiusLarge();
extern const char kOptionUseSmallMsgBubbleRadius[];
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
/*
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 "ui/effects/animations.h"
#include "base/timer.h"
#include "base/weak_ptr.h"
namespace style {
class palette;
struct colorizer;
} // namespace style
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Ui {
class ChatStyle;
struct ChatPaintContext;
struct BubblePattern;
struct ChatThemeGiftSymbol {
QRectF area;
float64 rotation = 0.;
};
struct ChatThemeBackground {
QString key;
QImage prepared;
QImage preparedForTiled;
QImage gradientForFill;
std::optional<QColor> colorForFill;
std::vector<QColor> colors;
std::vector<ChatThemeGiftSymbol> giftSymbols;
QImage giftSymbolFrame;
uint64_t giftId = 0;
float64 patternOpacity = 1.;
int gradientRotation = 0;
bool isPattern = false;
bool tile = false;
[[nodiscard]] bool waitingForNegativePattern() const {
return isPattern && prepared.isNull() && (patternOpacity < 0.);
}
};
bool operator==(const ChatThemeBackground &a, const ChatThemeBackground &b);
bool operator!=(const ChatThemeBackground &a, const ChatThemeBackground &b);
struct ChatThemeBackgroundData {
QString key;
QString path;
QByteArray bytes;
QImage giftSymbolFrame;
uint64 giftId = 0;
bool gzipSvg = false;
std::vector<QColor> colors;
bool isPattern = false;
float64 patternOpacity = 0.;
int darkModeDimming = 0;
bool isBlurred = false;
bool forDarkMode = false;
bool generateGradient = false;
int gradientRotation = 0;
};
struct ChatThemeBubblesData {
std::vector<QColor> colors;
std::optional<QColor> accent;
};
struct CacheBackgroundRequest {
ChatThemeBackground background;
QSize area;
int gradientRotationAdd = 0;
float64 gradientProgress = 1.;
explicit operator bool() const {
return !background.prepared.isNull()
|| !background.gradientForFill.isNull();
}
};
bool operator==(
const CacheBackgroundRequest &a,
const CacheBackgroundRequest &b);
bool operator!=(
const CacheBackgroundRequest &a,
const CacheBackgroundRequest &b);
struct CacheBackgroundResult {
QImage image;
QImage gradient;
QSize area;
int x = 0;
int y = 0;
QRect giftArea;
float64 giftRotation = 0;
bool waitingForNegativePattern = false;
};
[[nodiscard]] CacheBackgroundResult CacheBackground(
const CacheBackgroundRequest &request);
struct CachedBackground {
CachedBackground() = default;
CachedBackground(CacheBackgroundResult &&result);
QPixmap pixmap;
QSize area;
int x = 0;
int y = 0;
QRect giftArea;
float64 giftRotation = 0.;
mutable std::unique_ptr<Text::CustomEmoji> gift;
bool waitingForNegativePattern = false;
};
struct BackgroundState {
CachedBackground was;
CachedBackground now;
float64 shown = 1.;
};
struct ChatThemeKey {
uint64 id = 0;
bool dark = false;
explicit operator bool() const {
return (id != 0);
}
friend inline auto operator<=>(ChatThemeKey, ChatThemeKey) = default;
friend inline bool operator==(ChatThemeKey, ChatThemeKey) = default;
};
struct ChatThemeDescriptor {
ChatThemeKey key;
Fn<void(style::palette&)> preparePalette;
ChatThemeBackgroundData backgroundData;
ChatThemeBubblesData bubblesData;
bool basedOnDark = false;
};
class ChatTheme final : public base::has_weak_ptr {
public:
ChatTheme();
// Expected to be invoked on a background thread. Invokes callbacks there.
ChatTheme(ChatThemeDescriptor &&descriptor);
~ChatTheme();
[[nodiscard]] ChatThemeKey key() const;
[[nodiscard]] const style::palette *palette() const {
return _palette.get();
}
void setBackground(ChatThemeBackground &&background);
void updateBackgroundImageFrom(ChatThemeBackground &&background);
[[nodiscard]] const ChatThemeBackground &background() const {
return _mutableBackground;
}
void setBubblesBackground(QImage image);
[[nodiscard]] const BubblePattern *bubblesBackgroundPattern() const {
return _bubblesBackgroundPattern.get();
}
void finishCreateOnMain(); // Called on_main after setBubblesBackground.
[[nodiscard]] ChatPaintContext preparePaintContext(
not_null<const ChatStyle*> st,
QRect viewport,
QRect clip,
bool paused);
[[nodiscard]] const BackgroundState &backgroundState(QSize area);
void clearBackgroundState();
[[nodiscard]] rpl::producer<> repaintBackgroundRequests() const;
void rotateComplexGradientBackground();
[[nodiscard]] CacheBackgroundRequest cacheBackgroundRequest(
QSize area,
int addRotation = 0) const;
private:
void cacheBackground();
void cacheBackgroundNow();
void cacheBackgroundAsync(
const CacheBackgroundRequest &request,
Fn<void(CacheBackgroundResult&&)> done = nullptr);
void setCachedBackground(CacheBackgroundResult &&cached);
[[nodiscard]] bool readyForBackgroundRotation() const;
void generateNextBackgroundRotation();
void cacheBubbles();
void cacheBubblesNow();
void cacheBubblesAsync(
const CacheBackgroundRequest &request);
[[nodiscard]] CacheBackgroundRequest cacheBubblesRequest(
QSize area) const;
[[nodiscard]] style::colorizer bubblesAccentColorizer(
const QColor &accent) const;
void adjustPalette(const ChatThemeDescriptor &descriptor);
void set(const style::color &my, const QColor &color);
void adjust(const style::color &my, const QColor &by);
void adjust(const style::color &my, const style::colorizer &by);
ChatThemeKey _key;
std::unique_ptr<style::palette> _palette;
ChatThemeBackground _mutableBackground;
BackgroundState _backgroundState;
Animations::Simple _backgroundFade;
CacheBackgroundRequest _backgroundCachingRequest;
CacheBackgroundRequest _nextCachingRequest;
CacheBackgroundResult _backgroundNext;
int _backgroundVersion = 0;
QSize _cacheBackgroundArea;
crl::time _lastBackgroundAreaChangeTime = 0;
std::optional<base::Timer> _cacheBackgroundTimer;
CachedBackground _bubblesBackground;
QImage _bubblesBackgroundPrepared;
CacheBackgroundRequest _bubblesCachingRequest;
QSize _cacheBubblesArea;
crl::time _lastBubblesAreaChangeTime = 0;
std::optional<base::Timer> _cacheBubblesTimer;
std::unique_ptr<BubblePattern> _bubblesBackgroundPattern;
rpl::event_stream<> _repaintBackgroundRequests;
rpl::lifetime _lifetime;
};
struct ChatBackgroundRects {
QRect from;
QRect to;
};
[[nodiscard]] ChatBackgroundRects ComputeChatBackgroundRects(
QSize fillSize,
QSize imageSize);
[[nodiscard]] QColor CountAverageColor(const QImage &image);
[[nodiscard]] QColor CountAverageColor(const std::vector<QColor> &colors);
[[nodiscard]] bool IsPatternInverted(
const std::vector<QColor> &background,
float64 patternOpacity);
[[nodiscard]] QColor ThemeAdjustedColor(QColor original, QColor background);
[[nodiscard]] QImage PreprocessBackgroundImage(QImage image);
[[nodiscard]] std::optional<QColor> CalculateImageMonoColor(
const QImage &image);
[[nodiscard]] QImage PrepareImageForTiled(const QImage &prepared);
struct BackgroundImageFields {
QImage image;
std::vector<ChatThemeGiftSymbol> giftSymbols;
};
[[nodiscard]] BackgroundImageFields ReadBackgroundImage(
const QString &path,
const QByteArray &content,
bool gzipSvg,
bool findGiftSymbols = false);
[[nodiscard]] QImage GenerateBackgroundImage(
QSize size,
const std::vector<QColor> &bg,
int gradientRotation,
float64 patternOpacity = 1.,
Fn<void(QPainter&,bool)> drawPattern = nullptr);
[[nodiscard]] QImage InvertPatternImage(QImage pattern);
[[nodiscard]] QImage PreparePatternImage(
QImage pattern,
const std::vector<QColor> &bg,
int gradientRotation,
float64 patternOpacity);
[[nodiscard]] QImage PrepareBlurredBackground(QImage image);
[[nodiscard]] QImage GenerateDitheredGradient(
const std::vector<QColor> &colors,
int rotation);
[[nodiscard]] ChatThemeBackground PrepareBackgroundImage(
const ChatThemeBackgroundData &data);
[[nodiscard]] QImage PrepareGiftSymbol(
const std::unique_ptr<Text::CustomEmoji> &emoji);
} // namespace Ui

View File

@@ -0,0 +1,288 @@
/*
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 "ui/chat/chats_filter_tag.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/emoji_config.h"
#include "ui/integration.h"
#include "ui/painter.h"
#include "styles/style_dialogs.h"
namespace Ui {
namespace {
class ScaledSimpleEmoji final : public Ui::Text::CustomEmoji {
public:
ScaledSimpleEmoji(EmojiPtr emoji);
int width() override;
QString entityData() override;
void paint(QPainter &p, const Context &context) override;
void unload() override;
bool ready() override;
bool readyInDefaultState() override;
private:
const EmojiPtr _emoji;
QImage _frame;
QPoint _shift;
};
class ScaledCustomEmoji final : public Ui::Text::CustomEmoji {
public:
ScaledCustomEmoji(std::unique_ptr<Ui::Text::CustomEmoji> wrapped);
int width() override;
QString entityData() override;
void paint(QPainter &p, const Context &context) override;
void unload() override;
bool ready() override;
bool readyInDefaultState() override;
private:
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
QImage _frame;
QPoint _shift;
};
[[nodiscard]] int ScaledSize() {
return st::dialogRowFilterTagStyle.font->height - 2 * st::lineWidth;
}
ScaledSimpleEmoji::ScaledSimpleEmoji(EmojiPtr emoji)
: _emoji(emoji) {
}
int ScaledSimpleEmoji::width() {
return ScaledSize();
}
QString ScaledSimpleEmoji::entityData() {
return u"scaled-simple:"_q + _emoji->text();
}
void ScaledSimpleEmoji::paint(QPainter &p, const Context &context) {
if (_frame.isNull()) {
const auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize);
const auto xskip = (st::emojiSize - adjusted) / 2;
const auto yskip = xskip + (width() - st::emojiSize) / 2;
_shift = { xskip, yskip };
const auto ratio = style::DevicePixelRatio();
const auto large = Emoji::GetSizeLarge();
const auto size = QSize(large, large);
_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
_frame.fill(Qt::transparent);
auto p = QPainter(&_frame);
Emoji::Draw(p, _emoji, large, 0, 0);
p.end();
_frame = _frame.scaled(
QSize(width(), width()) * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
p.drawImage(context.position - _shift, _frame);
}
void ScaledSimpleEmoji::unload() {
}
bool ScaledSimpleEmoji::ready() {
return true;
}
bool ScaledSimpleEmoji::readyInDefaultState() {
return true;
}
ScaledCustomEmoji::ScaledCustomEmoji(
std::unique_ptr<Ui::Text::CustomEmoji> wrapped)
: _wrapped(std::move(wrapped)) {
}
int ScaledCustomEmoji::width() {
return ScaledSize();
}
QString ScaledCustomEmoji::entityData() {
return u"scaled-custom:"_q + _wrapped->entityData();
}
void ScaledCustomEmoji::paint(QPainter &p, const Context &context) {
if (_frame.isNull()) {
if (!_wrapped->ready()) {
return;
}
const auto ratio = style::DevicePixelRatio();
const auto large = Emoji::GetSizeLarge();
const auto largeadjust = Text::AdjustCustomEmojiSize(large / ratio);
const auto size = QSize(largeadjust, largeadjust) * ratio;
_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
_frame.fill(Qt::transparent);
auto p = QPainter(&_frame);
p.translate(-context.position);
const auto was = context.internal.forceFirstFrame;
context.internal.forceFirstFrame = true;
_wrapped->paint(p, context);
context.internal.forceFirstFrame = was;
p.end();
const auto smalladjust = Text::AdjustCustomEmojiSize(width());
_frame = _frame.scaled(
QSize(smalladjust, smalladjust) * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
_wrapped->unload();
const auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize);
const auto xskip = (st::emojiSize - adjusted) / 2;
const auto yskip = xskip + (width() - st::emojiSize) / 2;
const auto add = (width() - smalladjust) / 2;
_shift = QPoint(xskip, yskip) - QPoint(add, add);
}
p.drawImage(context.position - _shift, _frame);
}
void ScaledCustomEmoji::unload() {
_wrapped->unload();
}
bool ScaledCustomEmoji::ready() {
return !_frame.isNull() || _wrapped->ready();
}
bool ScaledCustomEmoji::readyInDefaultState() {
return !_frame.isNull() || _wrapped->ready();
}
[[nodiscard]] TextWithEntities PrepareSmallEmojiText(
TextWithEntities text,
ChatsFilterTagContext &context) {
auto i = text.entities.begin();
auto ch = text.text.constData();
context.loading = false;
const auto end = text.text.constData() + text.text.size();
const auto adjust = [&](EntityInText &entity) {
if (entity.type() != EntityType::CustomEmoji) {
return;
}
const auto data = entity.data();
if (data.startsWith(u"scaled-simple:"_q)) {
return;
}
auto &emoji = context.emoji[data];
if (!emoji) {
emoji = Text::MakeCustomEmoji(data, context.textContext);
}
if (!emoji->ready()) {
context.loading = true;
}
entity = EntityInText(
entity.type(),
entity.offset(),
entity.length(),
u"scaled-custom:"_q + entity.data());
};
const auto till = [](EntityInText &entity) {
return entity.offset() + entity.length();
};
while (ch != end) {
auto emojiLength = 0;
if (const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength)) {
const auto f = int(ch - text.text.constData());
const auto l = f + emojiLength;
while (i != text.entities.end() && till(*i) <= f) {
adjust(*i);
++i;
}
ch += emojiLength;
if (i != text.entities.end() && i->offset() < l) {
continue;
}
i = text.entities.insert(i, EntityInText{
EntityType::CustomEmoji,
f,
emojiLength,
u"scaled-simple:"_q + emoji->text(),
});
} else {
++ch;
}
}
for (; i != text.entities.end(); ++i) {
adjust(*i);
}
return text;
}
} // namespace
QImage ChatsFilterTag(
const TextWithEntities &text,
ChatsFilterTagContext &context) {
const auto &roundedFont = st::dialogRowFilterTagStyle.font;
const auto additionalWidth = roundedFont->spacew * 3;
auto rich = Text::String(
st::dialogRowFilterTagStyle,
PrepareSmallEmojiText(text, context),
kMarkupTextOptions,
kQFixedMax,
context.textContext);
const auto roundedWidth = rich.maxWidth() + additionalWidth;
const auto rect = QRect(0, 0, roundedWidth, roundedFont->height);
auto cache = QImage(
rect.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(style::DevicePixelRatio());
cache.fill(Qt::transparent);
{
auto p = QPainter(&cache);
const auto pen = QPen(context.active
? st::dialogsBgActive->c
: context.color);
p.setPen(Qt::NoPen);
p.setBrush(context.active
? st::dialogsTextFgActive->c
: anim::with_alpha(pen.color(), .15));
{
auto hq = PainterHighQualityEnabler(p);
const auto radius = roundedFont->height / 3.;
p.drawRoundedRect(rect, radius, radius);
}
p.setPen(pen);
p.setFont(roundedFont);
const auto dx = (rect.width() - rich.maxWidth()) / 2;
const auto dy = (rect.height() - roundedFont->height) / 2;
rich.draw(p, {
.position = rect.topLeft() + QPoint(dx, dy),
.availableWidth = rich.maxWidth(),
});
}
return cache;
}
std::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(EmojiPtr emoji) {
return std::make_unique<ScaledSimpleEmoji>(emoji);
}
std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(
std::unique_ptr<Text::CustomEmoji> wrapped) {
return std::make_unique<ScaledCustomEmoji>(std::move(wrapped));
}
} // namespace Ui

View File

@@ -0,0 +1,33 @@
/*
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 "emoji.h"
#include "ui/text/text.h"
namespace Ui {
struct ChatsFilterTagContext {
base::flat_map<QString, std::unique_ptr<Text::CustomEmoji>> emoji;
Text::MarkedContext textContext;
QColor color;
bool active = false;
bool loading = false;
};
[[nodiscard]] QImage ChatsFilterTag(
const TextWithEntities &text,
ChatsFilterTagContext &context);
[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(
EmojiPtr emoji);
[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(
std::unique_ptr<Text::CustomEmoji> wrapped);
} // namespace Ui

View File

@@ -0,0 +1,352 @@
/*
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 "ui/chat/choose_send_as.h"
#include "boxes/peer_list_box.h"
#include "data/data_group_call.h"
#include "data/data_peer.h"
#include "data/data_channel.h"
#include "data/data_peer_values.h"
#include "history/history.h"
#include "ui/controls/send_as_button.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "window/window_session_controller.h"
#include "main/main_session.h"
#include "main/session/send_as_peers.h"
#include "lang/lang_keys.h"
#include "settings/settings_premium.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace {
class Row final : public PeerListRow {
public:
explicit Row(const Main::SendAsPeer &sendAsPeer);
int paintNameIconGetWidth(
Painter &p,
Fn<void()> repaint,
crl::time now,
int nameLeft,
int nameTop,
int nameWidth,
int availableWidth,
int outerWidth,
bool selected) override;
private:
bool _premiumRequired = false;
};
class ListController final : public PeerListController {
public:
ListController(
std::vector<Main::SendAsPeer> list,
not_null<PeerData*> selected);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
[[nodiscard]] rpl::producer<not_null<PeerData*>> clicked() const;
private:
std::unique_ptr<Row> createRow(const Main::SendAsPeer &sendAsPeer);
std::vector<Main::SendAsPeer> _list;
not_null<PeerData*> _selected;
rpl::event_stream<not_null<PeerData*>> _clicked;
};
Row::Row(const Main::SendAsPeer &sendAsPeer)
: PeerListRow(sendAsPeer.peer)
, _premiumRequired(sendAsPeer.premiumRequired) {
}
int Row::paintNameIconGetWidth(
Painter &p,
Fn<void()> repaint,
crl::time now,
int nameLeft,
int nameTop,
int nameWidth,
int availableWidth,
int outerWidth,
bool selected) {
if (_premiumRequired && !peer()->session().premium()) {
const auto &icon = st::emojiPremiumRequired;
availableWidth -= icon.width();
const auto x = nameLeft + std::min(nameWidth, availableWidth);
icon.paint(p, x, nameTop, outerWidth);
return icon.width();
}
return PeerListRow::paintNameIconGetWidth(
p,
std::move(repaint),
now,
nameLeft,
nameTop,
nameWidth,
availableWidth,
outerWidth,
selected);
}
ListController::ListController(
std::vector<Main::SendAsPeer> list,
not_null<PeerData*> selected)
: PeerListController()
, _list(std::move(list))
, _selected(selected) {
Data::AmPremiumValue(
&selected->session()
) | rpl::skip(1) | rpl::on_next([=] {
const auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count; ++i) {
delegate()->peerListUpdateRow(
delegate()->peerListRowAt(i));
}
}, lifetime());
}
Main::Session &ListController::session() const {
return _selected->session();
}
std::unique_ptr<Row> ListController::createRow(
const Main::SendAsPeer &sendAsPeer) {
auto result = std::make_unique<Row>(sendAsPeer);
if (sendAsPeer.peer->isSelf()) {
result->setCustomStatus(
tr::lng_group_call_join_as_personal(tr::now));
} else if (sendAsPeer.peer->isMegagroup()) {
result->setCustomStatus(tr::lng_send_as_anonymous_admin(tr::now));
} else if (const auto channel = sendAsPeer.peer->asChannel()) {
result->setCustomStatus(tr::lng_chat_status_subscribers(
tr::now,
lt_count,
channel->membersCount()));
}
return result;
}
void ListController::prepare() {
delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
for (const auto &sendAsPeer : _list) {
auto row = createRow(sendAsPeer);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (sendAsPeer.peer == _selected) {
delegate()->peerListSetRowChecked(raw, true);
raw->finishCheckedAnimation();
}
}
delegate()->peerListRefreshRows();
}
void ListController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
if (peer == _selected) {
return;
}
_clicked.fire_copy(peer);
}
rpl::producer<not_null<PeerData*>> ListController::clicked() const {
return _clicked.events();
}
} // namespace
void ChooseSendAsBox(
not_null<GenericBox*> box,
const style::ChooseSendAs &st,
std::vector<Main::SendAsPeer> list,
not_null<PeerData*> chosen,
Fn<bool(not_null<PeerData*>)> done) {
Expects(ranges::contains(list, chosen, &Main::SendAsPeer::peer));
Expects(done != nullptr);
box->setWidth(st::groupCallJoinAsWidth);
box->setTitle(tr::lng_send_as_title());
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_group_call_join_as_about(),
st.label));
auto &lifetime = box->lifetime();
const auto delegate = lifetime.make_state<
PeerListContentDelegateSimple
>();
const auto controller = lifetime.make_state<ListController>(
list,
chosen);
controller->setStyleOverrides(&st.list, nullptr);
controller->clicked(
) | rpl::on_next([=](not_null<PeerData*> peer) {
const auto weak = base::make_weak(box);
if (done(peer) && weak) {
box->closeBox();
}
}, box->lifetime());
const auto content = box->addRow(
object_ptr<PeerListContent>(box, controller),
style::margins());
delegate->setContent(content);
controller->setDelegate(delegate);
box->addButton(tr::lng_box_done(), [=] { box->closeBox(); });
}
void SetupSendAsButton(
not_null<SendAsButton*> button,
const style::ChooseSendAs &st,
rpl::producer<PeerData*> active,
std::shared_ptr<ChatHelpers::Show> show) {
using namespace rpl::mappers;
const auto current = button->lifetime().make_state<
rpl::variable<PeerData*>
>(std::move(active));
button->setClickedCallback([=, &st] {
const auto peer = current->current();
if (!peer) {
return;
}
const auto key = Main::SendAsKey{ peer, Main::SendAsType::Message };
const auto session = &peer->session();
const auto &list = session->sendAsPeers().list(key);
if (list.size() < 2) {
return;
}
const auto done = [=](not_null<PeerData*> sendAs) {
const auto i = ranges::find(
list,
sendAs,
&Main::SendAsPeer::peer);
if (i != end(list)
&& i->premiumRequired
&& !sendAs->session().premium()) {
Settings::ShowPremiumPromoToast(
show,
tr::lng_send_as_premium_required(
tr::now,
lt_link,
tr::link(
tr::bold(
tr::lng_send_as_premium_required_link(
tr::now))),
tr::marked),
u"send_as"_q);
return false;
}
session->sendAsPeers().saveChosen(peer, sendAs);
return true;
};
show->show(Box(
Ui::ChooseSendAsBox,
st,
list,
session->sendAsPeers().resolveChosen(peer),
done));
});
const auto size = st.button.size;
auto userpic = current->value(
) | rpl::filter([=](PeerData *peer) {
return peer && peer->isChannel();
}) | rpl::map([=](not_null<PeerData*> peer) {
const auto channel = peer->asChannel();
auto updates = rpl::single(
rpl::empty
) | rpl::then(channel->session().sendAsPeers().updated(
) | rpl::filter(
_1 == Main::SendAsKey(channel, Main::SendAsType::Message)
) | rpl::to_empty);
return rpl::combine(
std::move(updates),
channel->adminRightsValue()
) | rpl::map([=] {
return channel->session().sendAsPeers().resolveChosen(channel);
}) | rpl::distinct_until_changed(
) | rpl::map([=](not_null<PeerData*> chosen) {
return Data::PeerUserpicImageValue(
chosen,
size * style::DevicePixelRatio());
}) | rpl::flatten_latest();
}) | rpl::flatten_latest();
std::move(
userpic
) | rpl::on_next([=](QImage &&userpic) {
button->setUserpic(std::move(userpic));
}, button->lifetime());
}
void SetupSendAsButton(
not_null<SendAsButton*> button,
const style::ChooseSendAs &st,
std::shared_ptr<Data::GroupCall> videoStream,
std::shared_ptr<ChatHelpers::Show> show) {
const auto peer = videoStream->peer();
const auto type = Main::SendAsType::VideoStream;
const auto key = Main::SendAsKey{ peer, type };
button->setClickedCallback([=, &st] {
const auto session = &peer->session();
const auto &list = session->sendAsPeers().list(key);
if (list.size() < 2) {
return;
}
const auto done = [=](not_null<PeerData*> sendAs) {
videoStream->saveSendAs(sendAs);
return true;
};
show->show(Box(
Ui::ChooseSendAsBox,
st,
list,
videoStream->resolveSendAs(),
done));
});
const auto size = st.button.size;
auto userpic = videoStream->sendAsValue(
) | rpl::distinct_until_changed(
) | rpl::map([=](not_null<PeerData*> chosen) {
return Data::PeerUserpicImageValue(
chosen,
size * style::DevicePixelRatio());
}) | rpl::flatten_latest();
std::move(
userpic
) | rpl::on_next([=](QImage &&userpic) {
button->setUserpic(std::move(userpic));
}, button->lifetime());
}
void SetupSendAsButton(
not_null<SendAsButton*> button,
const style::ChooseSendAs &st,
not_null<Window::SessionController*> window) {
auto active = window->activeChatValue(
) | rpl::map([=](Dialogs::Key key) {
return key.history() ? key.history()->peer.get() : nullptr;
});
SetupSendAsButton(button, st, std::move(active), window->uiShow());
}
} // namespace Ui

View File

@@ -0,0 +1,63 @@
/*
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 "base/object_ptr.h"
#include "ui/layers/generic_box.h"
class PeerData;
namespace style {
struct ChooseSendAs;
} // namespace style
namespace Data {
class GroupCall;
} // namespace Data
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Main {
struct SendAsPeer;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class SendAsButton;
void ChooseSendAsBox(
not_null<GenericBox*> box,
const style::ChooseSendAs &st,
std::vector<Main::SendAsPeer> list,
not_null<PeerData*> chosen,
Fn<bool(not_null<PeerData*>)> done);
void SetupSendAsButton(
not_null<SendAsButton*> button,
const style::ChooseSendAs &st,
rpl::producer<PeerData*> active,
std::shared_ptr<ChatHelpers::Show> show);
void SetupSendAsButton(
not_null<SendAsButton*> button,
const style::ChooseSendAs &st,
std::shared_ptr<Data::GroupCall> videoStream,
std::shared_ptr<ChatHelpers::Show> show);
void SetupSendAsButton(
not_null<SendAsButton*> button,
const style::ChooseSendAs &st,
not_null<Window::SessionController*> window);
} // namespace Ui

View File

@@ -0,0 +1,807 @@
/*
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 "ui/chat/choose_theme_controller.h"
#include "boxes/background_box.h"
#include "boxes/transfer_gift_box.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/rp_widget.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_theme.h"
#include "ui/chat/message_bubble.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/painter.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "window/themes/window_theme.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_session.h"
#include "data/data_peer.h"
#include "data/data_cloud_themes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "lang/lang_keys.h"
#include "apiwrap.h"
#include "styles/style_widgets.h"
#include "styles/style_layers.h" // boxTitle.
#include "styles/style_settings.h"
#include "styles/style_window.h"
#include <QtWidgets/QApplication>
namespace Ui {
namespace {
const auto kDisableElement = [] { return u"disable"_q; };
struct Preview {
QImage preview;
QRect userpic;
};
[[nodiscard]] Preview GeneratePreview(
not_null<Ui::ChatTheme*> theme,
const std::shared_ptr<Ui::DynamicImage> &takenUserpic) {
const auto &background = theme->background();
const auto &colors = background.colors;
const auto size = st::chatThemePreviewSize;
auto prepared = background.prepared;
const auto paintPattern = [&](QPainter &p, bool inverted) {
if (prepared.isNull()) {
return;
}
const auto w = prepared.width();
const auto h = prepared.height();
const auto scaled = size.scaled(
st::windowMinWidth / 2,
st::windowMinHeight / 2,
Qt::KeepAspectRatio);
const auto use = (scaled.width() > w || scaled.height() > h)
? scaled.scaled({ w, h }, Qt::KeepAspectRatio)
: scaled;
const auto good = QSize(
std::max(use.width(), 1),
std::max(use.height(), 1));
auto small = prepared.copy(QRect(
QPoint(
(w - good.width()) / 2,
(h - good.height()) / 2),
good));
if (inverted) {
small = Ui::InvertPatternImage(std::move(small));
}
p.drawImage(
QRect(QPoint(), size * style::DevicePixelRatio()),
small);
};
auto userpic = QRect();
const auto fullsize = size * style::DevicePixelRatio();
auto result = background.waitingForNegativePattern()
? QImage(
fullsize,
QImage::Format_ARGB32_Premultiplied)
: Ui::GenerateBackgroundImage(
fullsize,
colors.empty() ? std::vector{ 1, QColor(0, 0, 0) } : colors,
background.gradientRotation,
background.patternOpacity,
paintPattern);
if (background.waitingForNegativePattern()) {
result.fill(Qt::black);
}
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&result);
const auto sent = QRect(
QPoint(
(size.width()
- st::chatThemeBubbleSize.width()
- st::chatThemeBubblePosition.x()),
st::chatThemeBubblePosition.y()),
st::chatThemeBubbleSize);
const auto received = QRect(
st::chatThemeBubblePosition.x(),
sent.y() + sent.height() + st::chatThemeBubbleSkip,
sent.width(),
sent.height());
const auto radius = st::chatThemeBubbleRadius;
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
if (const auto pattern = theme->bubblesBackgroundPattern()) {
auto bubble = pattern->pixmap.toImage().scaled(
sent.size() * style::DevicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation
).convertToFormat(QImage::Format_ARGB32_Premultiplied);
const auto corners = Images::CornersMask(radius);
p.drawImage(sent, Images::Round(std::move(bubble), corners));
} else {
p.setBrush(theme->palette()->msgOutBg()->c);
p.drawRoundedRect(sent, radius, radius);
}
p.setBrush(theme->palette()->msgInBg()->c);
p.drawRoundedRect(received, radius, radius);
if (takenUserpic) {
const auto border = 2 * st::lineWidth;
const auto inner = received.marginsRemoved(
{ border, border, border, border });
userpic = inner;
userpic.setWidth(userpic.height());
st::chatThemeGiftTaken.paintInCenter(
p,
QRect(
inner.x() + inner.width() - inner.height() - border,
inner.y(),
inner.height(),
inner.height()),
theme->palette()->msgFileInBg()->c);
}
}
return {
.preview = Images::Round(std::move(result), ImageRoundRadius::Large),
.userpic = userpic,
};
}
[[nodiscard]] QImage GenerateEmptyPreview() {
auto result = QImage(
st::chatThemePreviewSize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(st::settingsThemeNotSupportedBg->c);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&result);
p.setPen(st::menuIconFg);
p.setFont(st::semiboldFont);
const auto top = st::chatThemeEmptyPreviewTop;
const auto width = st::chatThemePreviewSize.width();
const auto height = st::chatThemePreviewSize.height() - top;
p.drawText(
QRect(0, top, width, height),
tr::lng_chat_theme_none(tr::now),
style::al_top);
}
return Images::Round(std::move(result), ImageRoundRadius::Large);
}
} // namespace
struct ChooseThemeController::Entry {
QString token;
Ui::ChatThemeKey key;
std::shared_ptr<Ui::ChatTheme> theme;
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Data::UniqueGift> gift;
std::shared_ptr<Ui::DynamicImage> takenUserpic;
std::unique_ptr<Ui::Text::CustomEmoji> custom;
EmojiPtr emoji = nullptr;
QImage preview;
QRect userpic;
QRect geometry;
bool chosen = false;
};
ChooseThemeController::ChooseThemeController(
not_null<RpWidget*> parent,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer)
: _controller(window)
, _peer(peer)
, _wrap(std::make_unique<VerticalLayout>(parent))
, _topShadow(std::make_unique<PlainShadow>(parent))
, _content(_wrap->add(object_ptr<RpWidget>(_wrap.get())))
, _inner(CreateChild<RpWidget>(_content.get()))
, _disabledEmoji(Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\x8c")))
, _dark(Window::Theme::IsThemeDarkValue()) {
init(parent->sizeValue());
}
ChooseThemeController::~ChooseThemeController() {
_controller->clearPeerThemeOverride(_peer);
}
void ChooseThemeController::init(rpl::producer<QSize> outer) {
using namespace rpl::mappers;
const auto themes = &_controller->session().data().cloudThemes();
if (themes->myGiftThemesTokens().empty()) {
themes->myGiftThemesLoadMore();
}
const auto &list = themes->chatThemes();
if (!list.empty()) {
fill(list);
} else {
themes->refreshChatThemes();
themes->chatThemesUpdated(
) | rpl::take(1) | rpl::on_next([=] {
fill(themes->chatThemes());
}, lifetime());
}
const auto titleWrap = _wrap->insert(
0,
object_ptr<FixedHeightWidget>(
_wrap.get(),
st::boxTitle.style.font->height),
st::chatThemeTitlePadding);
auto title = CreateChild<FlatLabel>(
titleWrap,
tr::lng_chat_theme_title(),
st::boxTitle);
_wrap->paintRequest(
) | rpl::on_next([=](QRect clip) {
QPainter(_wrap.get()).fillRect(clip, st::windowBg);
}, lifetime());
const auto close = Ui::CreateChild<Ui::IconButton>(
_wrap.get(),
st::boxTitleClose);
close->setClickedCallback([=] { this->close(); });
rpl::combine(
_wrap->widthValue(),
titleWrap->positionValue()
) | rpl::on_next([=](int width, QPoint position) {
close->moveToRight(0, 0, width);
}, close->lifetime());
initButtons();
initList();
_inner->positionValue(
) | rpl::on_next([=](QPoint position) {
title->move(std::max(position.x(), 0), 0);
}, title->lifetime());
std::move(
outer
) | rpl::on_next([=](QSize outer) {
_wrap->resizeToWidth(outer.width());
_wrap->move(0, outer.height() - _wrap->height());
const auto line = st::lineWidth;
_topShadow->setGeometry(0, _wrap->y() - line, outer.width(), line);
}, lifetime());
rpl::combine(
_shouldBeShown.value(),
_forceHidden.value(),
_1 && !_2
) | rpl::on_next([=](bool shown) {
_wrap->setVisible(shown);
_topShadow->setVisible(shown);
}, lifetime());
}
void ChooseThemeController::initButtons() {
const auto controls = _wrap->add(object_ptr<RpWidget>(_wrap.get()));
const auto apply = CreateChild<RoundButton>(
controls,
tr::lng_chat_theme_apply(),
st::defaultLightButton);
apply->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
const auto choose = CreateChild<RoundButton>(
controls,
tr::lng_chat_theme_change_wallpaper(),
st::defaultLightButton);
choose->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
const auto &margin = st::chatThemeButtonMargin;
controls->resize(
margin.left() + choose->width() + margin.right(),
margin.top() + choose->height() + margin.bottom());
rpl::combine(
controls->widthValue(),
apply->widthValue(),
choose->widthValue(),
_chosen.value()
) | rpl::on_next([=](
int outer,
int applyWidth,
int chooseWidth,
QString chosen) {
const auto was = _peer->themeToken();
const auto now = (chosen == kDisableElement()) ? QString() : chosen;
const auto changed = (now != was);
apply->setVisible(changed);
choose->setVisible(!changed);
const auto shown = changed ? apply : choose;
const auto shownWidth = changed ? applyWidth : chooseWidth;
const auto inner = margin.left() + shownWidth + margin.right();
const auto left = (outer - inner) / 2;
shown->moveToLeft(left, margin.top());
}, controls->lifetime());
const auto setTheme = crl::guard(apply, [=](
const QString &token,
const std::shared_ptr<Ui::ChatTheme> &theme) {
SetPeerTheme(_controller, _peer, token, theme);
_controller->toggleChooseChatTheme(_peer);
});
const auto confirmTakeGiftTheme = crl::guard(apply, [=](
const QString &token,
const std::shared_ptr<Ui::ChatTheme> &theme,
not_null<PeerData*> nowHasTheme) {
_controller->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto confirmed = [=](Fn<void()> close) {
setTheme(token, theme);
close();
};
Ui::ConfirmBox(box, {
.text = tr::lng_chat_theme_gift_replace(
lt_name,
rpl::single(tr::bold(nowHasTheme->shortName())),
tr::marked),
.confirmed = confirmed,
.confirmText = tr::lng_box_yes(),
});
}));
});
apply->setClickedCallback([=] {
if (const auto chosen = findChosen()) {
const auto was = _peer->themeToken();
const auto now = chosen->key ? _chosen.current() : QString();
const auto user = chosen->gift
? chosen->gift->themeUser
: nullptr;
if (was != now) {
if (!user || user == _peer) {
setTheme(now, chosen->theme);
} else {
confirmTakeGiftTheme(now, chosen->theme, user);
}
} else {
_controller->toggleChooseChatTheme(_peer);
}
} else {
_controller->toggleChooseChatTheme(_peer);
}
});
choose->setClickedCallback([=] {
_controller->show(Box<BackgroundBox>(_controller, _peer));
});
}
void ChooseThemeController::paintEntry(QPainter &p, const Entry &entry) {
const auto geometry = entry.geometry;
p.drawImage(geometry, entry.preview);
if (const auto userpic = entry.takenUserpic.get()) {
userpic->subscribeToUpdates([=] {
_inner->update();
});
p.drawImage(
entry.userpic.translated(geometry.topLeft()),
userpic->image(entry.userpic.height()));
}
const auto size = Ui::Emoji::GetSizeLarge();
const auto factor = style::DevicePixelRatio();
const auto esize = size / factor;
const auto emojiLeft = geometry.x() + (geometry.width() - esize) / 2;
const auto emojiTop = geometry.y()
+ geometry.height()
- esize
- st::chatThemeEmojiBottom;
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto customSkip = (esize - customSize) / 2;
if (const auto emoji = entry.emoji) {
Ui::Emoji::Draw(p, emoji, size, emojiLeft, emojiTop);
} else if (const auto custom = entry.custom.get()) {
custom->paint(p, {
.textColor = st::windowFg->c,
.position = { emojiLeft + customSkip, emojiTop + customSkip },
});
}
if (entry.chosen) {
auto hq = PainterHighQualityEnabler(p);
auto pen = st::activeLineFg->p;
const auto width = st::defaultInputField.borderActive;
pen.setWidth(width);
p.setPen(pen);
const auto add = st::lineWidth + width;
p.drawRoundedRect(
entry.geometry.marginsAdded({ add, add, add, add }),
st::roundRadiusLarge + add,
st::roundRadiusLarge + add);
}
}
void ChooseThemeController::initList() {
_content->resize(
_content->width(),
(st::chatThemeEntryMargin.top()
+ st::chatThemePreviewSize.height()
+ st::chatThemeEntryMargin.bottom()));
_inner->setMouseTracking(true);
_inner->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(_inner.get());
for (const auto &entry : _entries) {
if (entry.preview.isNull() || !clip.intersects(entry.geometry)) {
continue;
}
paintEntry(p, entry);
}
}, lifetime());
const auto byPoint = [=](QPoint position) -> Entry* {
for (auto &entry : _entries) {
if (entry.geometry.contains(position)) {
return &entry;
}
}
return nullptr;
};
const auto chosenText = [=](const Entry *entry) {
if (!entry) {
return QString();
} else if (entry->key) {
return entry->token;
} else {
return kDisableElement();
}
};
_inner->events(
) | rpl::on_next([=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::MouseMove) {
const auto mouse = static_cast<QMouseEvent*>(event.get());
const auto skip = _inner->width() - _content->width();
if (skip <= 0) {
_dragStartPosition = _pressPosition = std::nullopt;
} else if (_pressPosition.has_value()
&& ((mouse->globalPos() - *_pressPosition).manhattanLength()
>= QApplication::startDragDistance())) {
_dragStartPosition = base::take(_pressPosition);
_dragStartInnerLeft = _inner->x();
}
if (_dragStartPosition.has_value()) {
const auto shift = mouse->globalPos().x()
- _dragStartPosition->x();
updateInnerLeft(_dragStartInnerLeft + shift);
} else {
_inner->setCursor(byPoint(mouse->pos())
? style::cur_pointer
: style::cur_default);
}
} else if (type == QEvent::MouseButtonPress) {
const auto mouse = static_cast<QMouseEvent*>(event.get());
if (mouse->button() == Qt::LeftButton) {
_pressPosition = mouse->globalPos();
}
_pressed = chosenText(byPoint(mouse->pos()));
} else if (type == QEvent::MouseButtonRelease) {
_pressPosition = _dragStartPosition = std::nullopt;
const auto mouse = static_cast<QMouseEvent*>(event.get());
const auto entry = byPoint(mouse->pos());
const auto chosen = chosenText(entry);
if (entry && chosen == _pressed && chosen != _chosen.current()) {
clearCurrentBackgroundState();
if (const auto was = findChosen()) {
was->chosen = false;
}
_chosen = chosen;
entry->chosen = true;
if (entry->theme || !entry->key) {
_controller->overridePeerTheme(
_peer,
entry->theme,
entry->token);
}
_inner->update();
}
_pressed = QString();
} else if (type == QEvent::Wheel) {
const auto wheel = static_cast<QWheelEvent*>(event.get());
const auto was = _inner->x();
updateInnerLeft((wheel->angleDelta().x() != 0)
? (was + (wheel->pixelDelta().x()
? wheel->pixelDelta().x()
: wheel->angleDelta().x()))
: (wheel->angleDelta().y() != 0)
? (was + (wheel->pixelDelta().y()
? wheel->pixelDelta().y()
: wheel->angleDelta().y()))
: was);
}
}, lifetime());
_content->events(
) | rpl::on_next([=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::KeyPress) {
const auto key = static_cast<QKeyEvent*>(event.get());
if (key->key() == Qt::Key_Escape) {
close();
}
}
}, lifetime());
rpl::combine(
_content->widthValue(),
_inner->widthValue()
) | rpl::on_next([=](int content, int inner) {
if (!content || !inner) {
return;
} else if (!_entries.empty() && !_initialInnerLeftApplied) {
applyInitialInnerLeft();
} else {
updateInnerLeft(_inner->x());
}
}, lifetime());
}
void ChooseThemeController::applyInitialInnerLeft() {
if (const auto chosen = findChosen()) {
updateInnerLeft(
_content->width() / 2 - chosen->geometry.center().x());
}
_initialInnerLeftApplied = true;
}
void ChooseThemeController::updateInnerLeft(int now) {
const auto skip = _content->width() - _inner->width();
const auto clamped = (skip >= 0)
? (skip / 2)
: std::clamp(now, skip, 0);
_inner->move(clamped, 0);
const auto visibleTill = -clamped + _content->width();
if (_giftsFinishAt - visibleTill < _content->width()) {
_peer->owner().cloudThemes().myGiftThemesLoadMore();
}
}
void ChooseThemeController::close() {
if (const auto chosen = findChosen()) {
if (_peer->themeToken() != chosen->token) {
clearCurrentBackgroundState();
}
}
_controller->toggleChooseChatTheme(_peer);
}
void ChooseThemeController::clearCurrentBackgroundState() {
if (const auto entry = findChosen()) {
if (entry->theme) {
entry->theme->clearBackgroundState();
}
}
}
auto ChooseThemeController::findChosen() -> Entry* {
const auto chosen = _chosen.current();
if (chosen.isEmpty()) {
return nullptr;
}
for (auto &entry : _entries) {
if (!entry.key && chosen == kDisableElement()) {
return &entry;
} else if (chosen == entry.token) {
return &entry;
}
}
return nullptr;
}
auto ChooseThemeController::findChosen() const -> const Entry* {
return const_cast<ChooseThemeController*>(this)->findChosen();
}
void ChooseThemeController::fill(
const std::vector<Data::CloudTheme> &themes) {
if (themes.empty()) {
return;
}
const auto single = st::chatThemePreviewSize;
const auto skip = st::chatThemeEntrySkip;
const auto &margin = st::chatThemeEntryMargin;
const auto initial = _peer->themeToken();
if (initial.isEmpty()) {
_chosen = kDisableElement();
}
const auto cloudThemes = &_controller->session().data().cloudThemes();
rpl::combine(
_dark.value(),
rpl::single(
rpl::empty
) | rpl::then(cloudThemes->myGiftThemesUpdated())
) | rpl::on_next([=](bool dark, auto) {
if (!cloudThemes->myGiftThemesReady()) {
return;
}
clearCurrentBackgroundState();
if (_chosen.current().isEmpty() && !initial.isEmpty()) {
_chosen = initial;
}
_cachingLifetime.destroy();
const auto old = base::take(_entries);
auto x = margin.left();
_entries.push_back({
.emoji = _disabledEmoji,
.preview = GenerateEmptyPreview(),
.geometry = QRect(QPoint(x, margin.top()), single),
.chosen = (_chosen.current() == kDisableElement()),
});
Assert(_entries.front().emoji != nullptr);
style::PaletteChanged(
) | rpl::on_next([=] {
_entries.front().preview = GenerateEmptyPreview();
}, _cachingLifetime);
const auto type = dark
? Data::CloudThemeType::Dark
: Data::CloudThemeType::Light;
x += single.width() + skip;
const auto owner = &_controller->session().data();
const auto manager = &owner->customEmojiManager();
const auto push = [&](
const Data::CloudTheme &theme,
const QString &token) {
if (token.isEmpty() || !theme.settings.contains(type)) {
return;
}
const auto key = ChatThemeKey{ theme.id, dark };
const auto isChosen = (_chosen.current() == token);
const auto themeUser = theme.unique
? theme.unique->themeUser
: nullptr;
_entries.push_back({
.token = token,
.key = key,
.gift = theme.unique,
.takenUserpic = (themeUser
? Ui::MakeUserpicThumbnail(themeUser, true)
: nullptr),
.custom = (theme.unique
? manager->create(
theme.unique->model.document,
[=] { _inner->update(); },
Data::CustomEmojiSizeTag::Large)
: nullptr),
.emoji = (theme.emoticon.isEmpty()
? nullptr
: Ui::Emoji::Find(theme.emoticon)),
.geometry = QRect(QPoint(x, skip), single),
.chosen = isChosen,
});
_controller->cachedChatThemeValue(
theme,
Data::WallPaper(0),
type
) | rpl::filter([=](const std::shared_ptr<ChatTheme> &data) {
return data && (data->key() == key);
}) | rpl::take(
1
) | rpl::on_next([=](std::shared_ptr<ChatTheme> &&data) {
const auto key = data->key();
const auto i = ranges::find(_entries, key, &Entry::key);
if (i == end(_entries)) {
return;
}
const auto theme = data.get();
const auto token = i->token;
i->theme = std::move(data);
auto generated = GeneratePreview(theme, i->takenUserpic);
i->preview = std::move(generated.preview);
i->userpic = generated.userpic;
if (_chosen.current() == token) {
_controller->overridePeerTheme(_peer, i->theme, token);
}
_inner->update();
if (!theme->background().isPattern
|| !theme->background().prepared.isNull()) {
return;
}
// Subscribe to pattern loading if needed.
theme->repaintBackgroundRequests(
) | rpl::filter([=] {
const auto i = ranges::find(
_entries,
key,
&Entry::key);
return (i == end(_entries))
|| !i->theme->background().prepared.isNull();
}) | rpl::take(1) | rpl::on_next([=] {
const auto i = ranges::find(
_entries,
key,
&Entry::key);
if (i == end(_entries)) {
return;
}
auto generated = GeneratePreview(theme, i->takenUserpic);
i->preview = std::move(generated.preview);
i->userpic = generated.userpic;
_inner->update();
}, _cachingLifetime);
}, _cachingLifetime);
x += single.width() + skip;
};
_giftsFinishAt = 0;
if (const auto now = cloudThemes->themeForToken(initial)) {
push(*now, initial);
}
for (const auto &token : cloudThemes->myGiftThemesTokens()) {
if (const auto found = cloudThemes->themeForToken(token)) {
if (token != initial) {
push(*found, token);
_giftsFinishAt = x;
}
}
}
for (const auto &theme : themes) {
if (const auto emoji = Ui::Emoji::Find(theme.emoticon)) {
const auto token = emoji->text();
if (token != initial) {
push(theme, token);
}
}
}
const auto full = x - skip + margin.right();
_inner->resize(
full,
margin.top() + single.height() + margin.bottom());
if (!_initialInnerLeftApplied && _content->width() > 0) {
applyInitialInnerLeft();
}
}, lifetime());
_shouldBeShown = true;
}
bool ChooseThemeController::shouldBeShown() const {
return _shouldBeShown.current();
}
rpl::producer<bool> ChooseThemeController::shouldBeShownValue() const {
return _shouldBeShown.value();
}
int ChooseThemeController::height() const {
return shouldBeShown() ? _wrap->height() : 0;
}
void ChooseThemeController::hide() {
_forceHidden = true;
}
void ChooseThemeController::show() {
_forceHidden = false;
}
void ChooseThemeController::raise() {
_wrap->raise();
_topShadow->raise();
}
void ChooseThemeController::setFocus() {
_content->setFocus();
}
rpl::lifetime &ChooseThemeController::lifetime() {
return _wrap->lifetime();
}
} // namespace Ui

View File

@@ -0,0 +1,86 @@
/*
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
class PeerData;
namespace Window {
class SessionController;
} // namespace Window
namespace Data {
struct CloudTheme;
} // namespace Data
namespace Ui {
class RpWidget;
class PlainShadow;
class VerticalLayout;
class ChooseThemeController final {
public:
ChooseThemeController(
not_null<RpWidget*> parent,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer);
~ChooseThemeController();
[[nodiscard]] bool shouldBeShown() const;
[[nodiscard]] rpl::producer<bool> shouldBeShownValue() const;
[[nodiscard]] int height() const;
void hide();
void show();
void raise();
void setFocus();
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Entry;
void init(rpl::producer<QSize> outer);
void initButtons();
void initList();
void fill(const std::vector<Data::CloudTheme> &themes);
void close();
void clearCurrentBackgroundState();
void paintEntry(QPainter &p, const Entry &entry);
void applyInitialInnerLeft();
void updateInnerLeft(int now);
[[nodiscard]] Entry *findChosen();
[[nodiscard]] const Entry *findChosen() const;
const not_null<Window::SessionController*> _controller;
const not_null<PeerData*> _peer;
const std::unique_ptr<VerticalLayout> _wrap;
const std::unique_ptr<PlainShadow> _topShadow;
const not_null<RpWidget*> _content;
const not_null<RpWidget*> _inner;
const EmojiPtr _disabledEmoji = nullptr;
std::vector<Entry> _entries;
QString _pressed;
rpl::variable<QString> _chosen;
std::optional<QPoint> _pressPosition;
std::optional<QPoint> _dragStartPosition;
int _dragStartInnerLeft = 0;
int _giftsFinishAt = 0;
bool _initialInnerLeftApplied = false;
rpl::variable<bool> _shouldBeShown = false;
rpl::variable<bool> _forceHidden = false;
rpl::variable<bool> _dark = false;
rpl::lifetime _cachingLifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,74 @@
/*
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 "ui/chat/continuous_scroll.h"
#include <QScrollBar>
#include <QWheelEvent>
namespace Ui {
void ContinuousScroll::wheelEvent(QWheelEvent *e) {
if (_tracking
&& !e->angleDelta().isNull()
&& (e->angleDelta().y() < 0)
&& (scrollTopMax() == scrollTop())) {
_addContentRequests.fire({});
if (base::take(_contentAdded)) {
viewportEvent(e);
}
return;
}
ScrollArea::wheelEvent(e);
}
void ContinuousScroll::setTrackingContent(bool value) {
if (_tracking == value) {
return;
}
_tracking = value;
reconnect();
}
void ContinuousScroll::reconnect() {
if (!_tracking) {
_connection.release();
return;
}
const auto handleAction = [=](int action) {
const auto scroll = verticalScrollBar();
const auto step = (action == QAbstractSlider::SliderSingleStepAdd)
? scroll->singleStep()
: (action == QAbstractSlider::SliderPageStepAdd)
? scroll->pageStep()
: 0;
if (!action) {
return;
}
const auto newTop = scrollTop() + step;
if (newTop > scrollTopMax()) {
_addContentRequests.fire({});
if (base::take(_contentAdded)) {
scroll->setSliderPosition(newTop);
}
}
};
_connection = QObject::connect(
verticalScrollBar(),
&QAbstractSlider::actionTriggered,
handleAction);
}
void ContinuousScroll::contentAdded() {
_contentAdded = true;
}
rpl::producer<> ContinuousScroll::addContentRequests() const {
return _addContentRequests.events();
}
} // namespace Ui

View File

@@ -0,0 +1,41 @@
/*
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 "ui/widgets/scroll_area.h"
#include "base/qt_connection.h"
namespace Ui {
// This class is designed for seamless scrolling of
// on-demand augmented content.
class ContinuousScroll final : public ScrollArea {
public:
using ScrollArea::ScrollArea;
[[nodiscard]] rpl::producer<> addContentRequests() const;
void contentAdded();
void setTrackingContent(bool value);
protected:
void wheelEvent(QWheelEvent *e) override;
private:
void reconnect();
base::qt_connection _connection;
bool _contentAdded = false;
bool _tracking = false;
rpl::event_stream<> _addContentRequests;
};
} // namespace Ui

View File

@@ -0,0 +1,68 @@
/*
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 "ui/chat/forward_options_box.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
namespace Ui {
void FillForwardOptions(
Fn<not_null<AbstractCheckView*>(
rpl::producer<QString> &&,
bool)> createView,
ForwardOptions options,
Fn<void(ForwardOptions)> optionsChanged,
rpl::lifetime &lifetime) {
Expects(optionsChanged != nullptr);
const auto names = createView(
(options.sendersCount == 1
? tr::lng_forward_show_sender
: tr::lng_forward_show_senders)(),
!options.dropNames);
const auto captions = options.captionsCount
? createView(
(options.captionsCount == 1
? tr::lng_forward_show_caption
: tr::lng_forward_show_captions)(),
!options.dropCaptions).get()
: nullptr;
const auto notify = [=] {
optionsChanged({
.sendersCount = options.sendersCount,
.captionsCount = options.captionsCount,
.dropNames = !names->checked(),
.dropCaptions = (captions && !captions->checked()),
});
};
names->checkedChanges(
) | rpl::on_next([=](bool showNames) {
if (showNames && captions && !captions->checked()) {
captions->setChecked(true, anim::type::normal);
} else {
notify();
}
}, lifetime);
if (captions) {
captions->checkedChanges(
) | rpl::on_next([=](bool showCaptions) {
if (!showCaptions && names->checked()) {
names->setChecked(false, anim::type::normal);
} else {
notify();
}
}, lifetime);
}
}
} // namespace Ui

View File

@@ -0,0 +1,31 @@
/*
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 "ui/layers/generic_box.h"
namespace Ui {
class AbstractCheckView;
struct ForwardOptions {
int sendersCount = 0;
int captionsCount = 0;
bool dropNames = false;
bool dropCaptions = false;
};
void FillForwardOptions(
Fn<not_null<AbstractCheckView*>(
rpl::producer<QString> &&,
bool)> createView,
ForwardOptions options,
Fn<void(ForwardOptions)> optionsChanged,
rpl::lifetime &lifetime);
} // namespace Ui

View File

@@ -0,0 +1,455 @@
/*
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 "ui/chat/group_call_bar.h"
#include "ui/chat/group_call_userpics.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_calls.h"
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
#include "styles/style_window.h" // st::columnMinimalWidthLeft
#include "styles/palette.h"
#include <QtGui/QtEvents>
#include <QtCore/QLocale>
namespace Ui {
GroupCallScheduledLeft::GroupCallScheduledLeft(TimeId date)
: _date(date)
, _datePrecise(computePreciseDate())
, _timer([=] { update(); }) {
update();
base::unixtime::updates(
) | rpl::on_next([=] {
restart();
}, _lifetime);
}
crl::time GroupCallScheduledLeft::computePreciseDate() const {
return crl::now() + (_date - base::unixtime::now()) * crl::time(1000);
}
void GroupCallScheduledLeft::setDate(TimeId date) {
if (_date == date) {
return;
}
_date = date;
restart();
}
void GroupCallScheduledLeft::restart() {
_datePrecise = computePreciseDate();
_timer.cancel();
update();
}
rpl::producer<QString> GroupCallScheduledLeft::text(Negative negative) const {
return (negative == Negative::Show)
? _text.value()
: _textNonNegative.value();
}
rpl::producer<bool> GroupCallScheduledLeft::late() const {
return _late.value();
}
void GroupCallScheduledLeft::update() {
const auto now = crl::now();
const auto duration = (_datePrecise - now);
const auto left = crl::time(base::SafeRound(std::abs(duration) / 1000.));
const auto late = (duration < 0) && (left > 0);
_late = late;
constexpr auto kDay = 24 * 60 * 60;
if (left >= kDay) {
const auto days = (left / kDay);
_textNonNegative = tr::lng_days(tr::now, lt_count, days);
_text = late
? tr::lng_days(tr::now, lt_count, -days)
: _textNonNegative.current();
} else {
const auto hours = left / (60 * 60);
const auto minutes = (left % (60 * 60)) / 60;
const auto seconds = (left % 60);
_textNonNegative = (hours > 0)
? (u"%1:%2:%3"_q
.arg(hours, 2, 10, QChar('0'))
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0')))
: (u"%1:%2"_q
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0')));
_text = (late ? QString(QChar(0x2212)) : QString())
+ _textNonNegative.current();
}
if (left >= kDay) {
_timer.callOnce((left % kDay) * crl::time(1000));
} else {
const auto fraction = (std::abs(duration) + 500) % 1000;
if (fraction < 400 || fraction > 600) {
const auto next = std::abs(duration) % 1000;
_timer.callOnce((duration < 0) ? (1000 - next) : next);
} else if (!_timer.isActive()) {
_timer.callEach(1000);
}
}
}
GroupCallBar::GroupCallBar(
not_null<QWidget*> parent,
rpl::producer<GroupCallBarContent> content,
rpl::producer<bool> &&hideBlobs)
: _wrap(parent, object_ptr<RpWidget>(parent))
, _inner(_wrap.entity())
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
, _userpics(std::make_unique<GroupCallUserpics>(
st::historyGroupCallUserpics,
std::move(hideBlobs),
[=] { updateUserpics(); })) {
_wrap.hide(anim::type::instant);
_shadow->hide();
_wrap.entity()->paintRequest(
) | rpl::on_next([=](QRect clip) {
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
}, lifetime());
_wrap.setAttribute(Qt::WA_OpaquePaintEvent);
auto copy = std::move(
content
) | rpl::start_spawning(_wrap.lifetime());
rpl::duplicate(
copy
) | rpl::on_next([=](GroupCallBarContent &&content) {
_content = content;
_userpics->update(_content.users, !_wrap.isHidden());
_inner->update();
refreshScheduledProcess();
}, lifetime());
if (!_open && !_join) {
refreshScheduledProcess();
}
std::move(
copy
) | rpl::map([=](const GroupCallBarContent &content) {
return !content.shown;
}) | rpl::on_next_done([=](bool hidden) {
_shouldBeShown = !hidden;
if (!_forceHidden) {
_wrap.toggle(_shouldBeShown, anim::type::normal);
}
}, [=] {
_forceHidden = true;
_wrap.toggle(false, anim::type::normal);
}, lifetime());
setupInner();
}
GroupCallBar::~GroupCallBar() = default;
void GroupCallBar::refreshOpenBrush() {
Expects(_open != nullptr);
const auto width = _open->width();
if (_openBrushForWidth == width) {
return;
}
auto gradient = QLinearGradient(QPoint(width, 0), QPoint(0, 0));
gradient.setStops(QGradientStops{
{ 0.0, st::groupCallForceMutedBar1->c },
{ .7, st::groupCallForceMutedBar2->c },
{ 1.0, st::groupCallForceMutedBar3->c }
});
_openBrushOverride = QBrush(std::move(gradient));
_openBrushForWidth = width;
_open->setBrushOverride(_openBrushOverride);
}
void GroupCallBar::refreshScheduledProcess() {
const auto date = _content.scheduleDate;
if (!date) {
if (_scheduledProcess) {
_scheduledProcess = nullptr;
_open = nullptr;
_openBrushForWidth = 0;
}
if (!_join) {
_join = std::make_unique<RoundButton>(
_inner.get(),
tr::lng_group_call_join(),
st::groupCallTopBarJoin);
setupRightButton(_join.get());
}
} else if (!_scheduledProcess) {
_scheduledProcess = std::make_unique<GroupCallScheduledLeft>(date);
_join = nullptr;
_open = std::make_unique<RoundButton>(
_inner.get(),
_scheduledProcess->text(GroupCallScheduledLeft::Negative::Show),
st::groupCallTopBarOpen);
setupRightButton(_open.get());
_open->widthValue(
) | rpl::on_next([=] {
refreshOpenBrush();
}, _open->lifetime());
} else {
_scheduledProcess->setDate(date);
}
}
void GroupCallBar::setupInner() {
_inner->resize(0, st::historyReplyHeight);
_inner->paintRequest(
) | rpl::on_next([=](QRect rect) {
auto p = Painter(_inner);
paint(p);
}, _inner->lifetime());
// Clicks.
_inner->setCursor(style::cur_pointer);
_inner->events(
) | rpl::filter([=](not_null<QEvent*> event) {
return (event->type() == QEvent::MouseButtonPress)
&& (static_cast<QMouseEvent*>(event.get())->button()
== Qt::LeftButton);
}) | rpl::map([=] {
return _inner->events(
) | rpl::filter([=](not_null<QEvent*> event) {
return (event->type() == QEvent::MouseButtonRelease);
}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {
return _inner->rect().contains(
static_cast<QMouseEvent*>(event.get())->pos());
});
}) | rpl::flatten_latest(
) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());
_wrap.geometryValue(
) | rpl::on_next([=](QRect rect) {
updateShadowGeometry(rect);
updateControlsGeometry(rect);
}, _inner->lifetime());
}
void GroupCallBar::setupRightButton(not_null<RoundButton*> button) {
button->setFullRadius(true);
rpl::combine(
_inner->widthValue(),
button->widthValue()
) | rpl::on_next([=](int outerWidth, int buttonWidth) {
// Skip shadow of the bar above.
const auto top = (st::historyReplyHeight
- st::lineWidth
- button->height()) / 2 + st::lineWidth;
const auto narrow = (outerWidth < st::columnMinimalWidthLeft / 2);
if (narrow) {
button->moveToLeft(
(outerWidth - buttonWidth) / 2,
top,
outerWidth);
} else {
button->moveToRight(top, top, outerWidth);
}
}, button->lifetime());
button->clicks() | rpl::start_to_stream(_joinClicks, button->lifetime());
}
void GroupCallBar::paint(Painter &p) {
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
const auto narrow = (_inner->width() < st::columnMinimalWidthLeft / 2);
if (!narrow) {
paintTitleAndStatus(p);
paintUserpics(p);
}
}
void GroupCallBar::paintTitleAndStatus(Painter &p) {
const auto left = st::topBarArrowPadding.right();
const auto titleTop = st::msgReplyPadding.top();
const auto textTop = titleTop + st::msgServiceNameFont->height;
const auto width = _inner->width();
const auto &font = st::defaultMessageBar.title.font;
p.setPen(st::defaultMessageBar.textFg);
p.setFont(font);
const auto available = (_join ? _join->x() : _open->x()) - left;
const auto titleWidth = font->width(_content.title);
p.drawTextLeft(
left,
titleTop,
width,
(!_content.scheduleDate
? (_content.livestream
? tr::lng_group_call_title_channel
: tr::lng_group_call_title)(tr::now)
: _content.title.isEmpty()
? (_content.livestream
? tr::lng_group_call_scheduled_title_channel
: tr::lng_group_call_scheduled_title)(tr::now)
: (titleWidth > available)
? font->elided(_content.title, available)
: _content.title));
p.setPen(st::historyStatusFg);
p.setFont(st::defaultMessageBar.text.font);
const auto when = [&] {
if (!_content.scheduleDate) {
return QString();
}
const auto parsed = base::unixtime::parse(_content.scheduleDate);
const auto date = parsed.date();
const auto time = QLocale().toString(
parsed.time(),
QLocale::ShortFormat);
const auto today = QDate::currentDate();
if (date == today) {
return tr::lng_group_call_starts_today(tr::now, lt_time, time);
} else if (date == today.addDays(1)) {
return tr::lng_group_call_starts_tomorrow(
tr::now,
lt_time,
time);
} else {
return tr::lng_group_call_starts_date(
tr::now,
lt_date,
langDayOfMonthFull(date),
lt_time,
time);
}
}();
p.drawTextLeft(
left,
textTop,
width,
(_content.scheduleDate
? (_content.title.isEmpty()
? tr::lng_group_call_starts_short
: _content.livestream
? tr::lng_group_call_starts_channel
: tr::lng_group_call_starts)(tr::now, lt_when, when)
: _content.count > 0
? tr::lng_group_call_members(
tr::now,
lt_count_decimal,
_content.count)
: tr::lng_group_call_no_members(tr::now)));
}
void GroupCallBar::paintUserpics(Painter &p) {
const auto size = st::historyGroupCallUserpics.size;
// Skip shadow of the bar above.
const auto top = (st::historyReplyHeight - st::lineWidth - size) / 2
+ st::lineWidth;
_userpics->paint(p, _inner->width() / 2, top, size);
}
void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
if (_shadow->isHidden() != hidden) {
_shadow->setVisible(!hidden);
}
}
void GroupCallBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {
_shadowGeometryPostprocess = std::move(postprocess);
updateShadowGeometry(_wrap.geometry());
}
void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {
const auto regular = QRect(
wrapGeometry.x(),
wrapGeometry.y() + wrapGeometry.height(),
wrapGeometry.width(),
st::lineWidth);
_shadow->setGeometry(_shadowGeometryPostprocess
? _shadowGeometryPostprocess(regular)
: regular);
}
void GroupCallBar::updateUserpics() {
const auto widget = _wrap.entity();
const auto middle = widget->width() / 2;
const auto width = _userpics->maxWidth();
widget->update(
(middle - width / 2),
0,
width,
widget->height());
}
void GroupCallBar::show() {
if (!_forceHidden) {
return;
}
_forceHidden = false;
if (_shouldBeShown) {
_wrap.show(anim::type::instant);
_shadow->show();
}
}
void GroupCallBar::hide() {
if (_forceHidden) {
return;
}
_forceHidden = true;
_wrap.hide(anim::type::instant);
_shadow->hide();
}
void GroupCallBar::raise() {
_wrap.raise();
_shadow->raise();
}
void GroupCallBar::finishAnimating() {
_wrap.finishAnimating();
}
void GroupCallBar::move(int x, int y) {
_wrap.move(x, y);
}
void GroupCallBar::resizeToWidth(int width) {
_wrap.entity()->resizeToWidth(width);
_inner->resizeToWidth(width);
}
int GroupCallBar::height() const {
return !_forceHidden
? _wrap.height()
: _shouldBeShown
? st::historyReplyHeight
: 0;
}
rpl::producer<int> GroupCallBar::heightValue() const {
return _wrap.heightValue();
}
rpl::producer<> GroupCallBar::barClicks() const {
return _barClicks.events();
}
rpl::producer<> GroupCallBar::joinClicks() const {
using namespace rpl::mappers;
return _joinClicks.events()
| rpl::filter(_1 == Qt::LeftButton)
| rpl::to_empty;
}
} // namespace Ui

View File

@@ -0,0 +1,120 @@
/*
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 "ui/wrap/slide_wrap.h"
#include "ui/effects/animations.h"
#include "base/object_ptr.h"
#include "base/timer.h"
class Painter;
namespace Ui {
class PlainShadow;
class RoundButton;
struct GroupCallUser;
class GroupCallUserpics;
struct GroupCallBarContent {
QString title;
TimeId scheduleDate = 0;
int count = 0;
bool shown = false;
bool livestream = false;
std::vector<GroupCallUser> users;
};
class GroupCallScheduledLeft final {
public:
enum class Negative {
Show,
Ignore,
};
explicit GroupCallScheduledLeft(TimeId date);
void setDate(TimeId date);
[[nodiscard]] rpl::producer<QString> text(Negative negative) const;
[[nodiscard]] rpl::producer<bool> late() const;
private:
[[nodiscard]] crl::time computePreciseDate() const;
void restart();
void update();
rpl::variable<QString> _text;
rpl::variable<QString> _textNonNegative;
rpl::variable<bool> _late;
TimeId _date = 0;
crl::time _datePrecise = 0;
base::Timer _timer;
rpl::lifetime _lifetime;
};
class GroupCallBar final {
public:
GroupCallBar(
not_null<QWidget*> parent,
rpl::producer<GroupCallBarContent> content,
rpl::producer<bool> &&hideBlobs);
~GroupCallBar();
void show();
void hide();
void raise();
void finishAnimating();
void setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);
void move(int x, int y);
void resizeToWidth(int width);
[[nodiscard]] int height() const;
[[nodiscard]] rpl::producer<int> heightValue() const;
[[nodiscard]] rpl::producer<> barClicks() const;
[[nodiscard]] rpl::producer<> joinClicks() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _wrap.lifetime();
}
private:
using User = GroupCallUser;
void refreshOpenBrush();
void refreshScheduledProcess();
void updateShadowGeometry(QRect wrapGeometry);
void updateControlsGeometry(QRect wrapGeometry);
void updateUserpics();
void setupInner();
void setupRightButton(not_null<RoundButton*> button);
void paint(Painter &p);
void paintTitleAndStatus(Painter &p);
void paintUserpics(Painter &p);
SlideWrap<> _wrap;
not_null<RpWidget*> _inner;
std::unique_ptr<RoundButton> _join;
std::unique_ptr<RoundButton> _open;
rpl::event_stream<Qt::MouseButton> _joinClicks;
QBrush _openBrushOverride;
int _openBrushForWidth = 0;
std::unique_ptr<PlainShadow> _shadow;
rpl::event_stream<> _barClicks;
Fn<QRect(QRect)> _shadowGeometryPostprocess;
bool _shouldBeShown = false;
bool _forceHidden = false;
GroupCallBarContent _content;
std::unique_ptr<GroupCallScheduledLeft> _scheduledProcess;
std::unique_ptr<GroupCallUserpics> _userpics;
};
} // namespace Ui

View File

@@ -0,0 +1,428 @@
/*
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 "ui/chat/group_call_userpics.h"
#include "ui/paint/blobs.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "base/random.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace {
constexpr auto kDuration = 160;
constexpr auto kMaxUserpics = 4;
constexpr auto kWideScale = 5;
constexpr auto kBlobsEnterDuration = crl::time(250);
constexpr auto kLevelDuration = 100. + 500. * 0.23;
constexpr auto kBlobScale = 0.605;
constexpr auto kMinorBlobFactor = 0.9f;
constexpr auto kUserpicMinScale = 0.8;
constexpr auto kMaxLevel = 1.;
constexpr auto kSendRandomLevelInterval = crl::time(100);
auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
return { {
{
.segmentsCount = 6,
.minScale = kBlobScale * kMinorBlobFactor,
.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
.speedScale = 1.,
.alpha = .5,
},
{
.segmentsCount = 8,
.minScale = kBlobScale,
.minRadius = (float)st::historyGroupCallBlobMinRadius,
.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
.speedScale = 1.,
.alpha = .2,
},
} };
}
} // namespace
struct GroupCallUserpics::BlobsAnimation {
BlobsAnimation(
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
float levelDuration,
float maxLevel)
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
}
Ui::Paint::Blobs blobs;
crl::time lastTime = 0;
crl::time lastSpeakingUpdateTime = 0;
float64 enter = 0.;
};
struct GroupCallUserpics::Userpic {
User data;
std::pair<uint64, uint64> cacheKey;
crl::time speakingStarted = 0;
QImage cache;
Animations::Simple leftAnimation;
Animations::Simple shownAnimation;
std::unique_ptr<BlobsAnimation> blobsAnimation;
int left = 0;
bool positionInited = false;
bool topMost = false;
bool hiding = false;
bool cacheMasked = false;
};
GroupCallUserpics::GroupCallUserpics(
const style::GroupCallUserpics &st,
rpl::producer<bool> &&hideBlobs,
Fn<void()> repaint)
: _st(st)
, _randomSpeakingTimer([=] { sendRandomLevels(); })
, _repaint(std::move(repaint)) {
const auto limit = kMaxUserpics;
const auto single = _st.size;
const auto shift = _st.shift;
// + 1 * single for the blobs.
_maxWidth = 2 * single + (limit - 1) * (single - shift);
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &userpic : _list) {
userpic.cache = QImage();
}
}, lifetime());
_speakingAnimation.init([=](crl::time now) {
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
&& (now - last >= kBlobsEnterDuration)) {
_speakingAnimation.stop();
}
for (auto &userpic : _list) {
if (const auto blobs = userpic.blobsAnimation.get()) {
blobs->blobs.updateLevel(now - blobs->lastTime);
blobs->lastTime = now;
}
}
if (const auto onstack = _repaint) {
onstack();
}
});
rpl::combine(
PowerSaving::OnValue(PowerSaving::kCalls),
std::move(hideBlobs)
) | rpl::on_next([=](bool disabled, bool deactivated) {
const auto hide = disabled || deactivated;
if (!(hide && _speakingAnimationHideLastTime)) {
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
}
_skipLevelUpdate = hide;
for (auto &userpic : _list) {
if (const auto blobs = userpic.blobsAnimation.get()) {
blobs->blobs.setLevel(0.);
}
}
if (!hide && !_speakingAnimation.animating()) {
_speakingAnimation.start();
}
_skipLevelUpdate = hide;
}, lifetime());
}
GroupCallUserpics::~GroupCallUserpics() = default;
void GroupCallUserpics::paint(QPainter &p, int x, int y, int size) {
const auto factor = style::DevicePixelRatio();
const auto &minScale = kUserpicMinScale;
for (auto &userpic : ranges::views::reverse(_list)) {
const auto shown = userpic.shownAnimation.value(
userpic.hiding ? 0. : 1.);
if (shown == 0.) {
continue;
}
validateCache(userpic);
p.setOpacity(shown);
const auto left = x + userpic.leftAnimation.value(userpic.left);
const auto blobs = userpic.blobsAnimation.get();
const auto shownScale = 0.5 + shown / 2.;
const auto scale = shownScale * (!blobs
? 1.
: (minScale
+ (1. - minScale) * (_speakingAnimationHideLastTime
? (1. - blobs->blobs.currentLevel())
: blobs->blobs.currentLevel())));
if (blobs) {
auto hq = PainterHighQualityEnabler(p);
const auto shift = QPointF(left + size / 2., y + size / 2.);
p.translate(shift);
blobs->blobs.paint(p, st::windowActiveTextFg);
p.translate(-shift);
p.setOpacity(1.);
}
if (std::abs(scale - 1.) < 0.001) {
const auto skip = ((kWideScale - 1) / 2) * size * factor;
p.drawImage(
QRect(left, y, size, size),
userpic.cache,
QRect(skip, skip, size * factor, size * factor));
} else {
auto hq = PainterHighQualityEnabler(p);
auto target = QRect(
left + (1 - kWideScale) / 2 * size,
y + (1 - kWideScale) / 2 * size,
kWideScale * size,
kWideScale * size);
auto shrink = anim::interpolate(
(1 - kWideScale) / 2 * size,
0,
scale);
auto margins = QMargins(shrink, shrink, shrink, shrink);
p.drawImage(target.marginsAdded(margins), userpic.cache);
}
}
p.setOpacity(1.);
const auto hidden = [](const Userpic &userpic) {
return userpic.hiding && !userpic.shownAnimation.animating();
};
_list.erase(ranges::remove_if(_list, hidden), end(_list));
}
int GroupCallUserpics::maxWidth() const {
return _maxWidth;
}
rpl::producer<int> GroupCallUserpics::widthValue() const {
return _width.value();
}
bool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {
if (userpic.cache.isNull()) {
return true;
} else if (userpic.hiding) {
return false;
} else if (userpic.cacheKey != userpic.data.userpicKey) {
return true;
}
const auto shouldBeMasked = !userpic.topMost;
if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
return true;
}
return !userpic.leftAnimation.animating();
}
void GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {
if (userpic.blobsAnimation) {
return;
}
userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
Blobs() | ranges::to_vector,
kLevelDuration,
kMaxLevel);
userpic.blobsAnimation->lastTime = crl::now();
}
void GroupCallUserpics::sendRandomLevels() {
if (_skipLevelUpdate) {
return;
}
for (auto &userpic : _list) {
if (const auto blobs = userpic.blobsAnimation.get()) {
const auto value = 30 + base::RandomIndex(70);
blobs->blobs.setLevel(float64(value) / 100.);
}
}
}
void GroupCallUserpics::validateCache(Userpic &userpic) {
if (!needCacheRefresh(userpic)) {
return;
}
const auto factor = style::DevicePixelRatio();
const auto size = _st.size;
const auto shift = _st.shift;
const auto full = QSize(size, size) * kWideScale * factor;
if (userpic.cache.isNull()) {
userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
userpic.cache.setDevicePixelRatio(factor);
}
userpic.cacheKey = userpic.data.userpicKey;
userpic.cacheMasked = !userpic.topMost;
userpic.cache.fill(Qt::transparent);
{
auto p = QPainter(&userpic.cache);
const auto skip = (kWideScale - 1) / 2 * size;
p.drawImage(QRect(skip, skip, size, size), userpic.data.userpic);
if (userpic.cacheMasked) {
auto hq = PainterHighQualityEnabler(p);
auto pen = QPen(Qt::transparent);
pen.setWidth(_st.stroke);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setBrush(Qt::transparent);
p.setPen(pen);
p.drawEllipse(skip - size + shift, skip, size, size);
}
}
}
void GroupCallUserpics::update(
const std::vector<GroupCallUser> &users,
bool visible) {
const auto idFromUserpic = [](const Userpic &userpic) {
return userpic.data.id;
};
// Use "topMost" as "willBeHidden" flag.
for (auto &userpic : _list) {
userpic.topMost = true;
}
for (const auto &user : users) {
const auto i = ranges::find(_list, user.id, idFromUserpic);
if (i == end(_list)) {
_list.push_back(Userpic{ user });
toggle(_list.back(), true);
continue;
}
i->topMost = false;
if (i->hiding) {
toggle(*i, true);
}
i->data = user;
// Put this one after the last we are not hiding.
for (auto j = end(_list) - 1; j != i; --j) {
if (!j->topMost) {
ranges::rotate(i, i + 1, j + 1);
break;
}
}
}
// Hide the ones that "willBeHidden" (currently having "topMost" flag).
// Set correct real values of "topMost" flag.
const auto userpicsBegin = begin(_list);
const auto userpicsEnd = end(_list);
auto markedTopMost = userpicsEnd;
auto hasBlobs = false;
for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
auto &userpic = *i;
if (userpic.data.speaking) {
ensureBlobsAnimation(userpic);
hasBlobs = true;
} else {
userpic.blobsAnimation = nullptr;
}
if (userpic.topMost) {
toggle(userpic, false);
userpic.topMost = false;
} else if (markedTopMost == userpicsEnd) {
userpic.topMost = true;
markedTopMost = i;
}
}
if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
// Bring the topMost userpic to the very beginning, above all hiding.
std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
}
updatePositions();
if (!hasBlobs) {
_randomSpeakingTimer.cancel();
_speakingAnimation.stop();
} else if (!_randomSpeakingTimer.isActive()) {
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
_speakingAnimation.start();
}
if (visible) {
recountAndRepaint();
} else {
finishAnimating();
}
}
void GroupCallUserpics::finishAnimating() {
for (auto &userpic : _list) {
userpic.shownAnimation.stop();
userpic.leftAnimation.stop();
}
recountAndRepaint();
}
void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
if (userpic.hiding == !shown && !userpic.shownAnimation.animating()) {
return;
}
userpic.hiding = !shown;
userpic.shownAnimation.start(
[=] { recountAndRepaint(); },
shown ? 0. : 1.,
shown ? 1. : 0.,
kDuration);
}
void GroupCallUserpics::updatePositions() {
const auto shownCount = ranges::count(_list, false, &Userpic::hiding);
if (!shownCount) {
return;
}
const auto single = _st.size;
const auto shift = _st.shift;
// + 1 * single for the blobs.
const auto fullWidth = single + (shownCount - 1) * (single - shift);
auto left = (_st.align & Qt::AlignLeft)
? 0
: (_st.align & Qt::AlignHCenter)
? (-fullWidth / 2)
: -fullWidth;
for (auto &userpic : _list) {
if (userpic.hiding) {
continue;
}
if (!userpic.positionInited) {
userpic.positionInited = true;
userpic.left = left;
} else if (userpic.left != left) {
userpic.leftAnimation.start(
_repaint,
userpic.left,
left,
kDuration);
userpic.left = left;
}
left += (single - shift);
}
}
void GroupCallUserpics::recountAndRepaint() {
auto width = 0;
auto maxShown = 0.;
for (const auto &userpic : _list) {
const auto shown = userpic.shownAnimation.value(
userpic.hiding ? 0. : 1.);
if (shown > maxShown) {
maxShown = shown;
}
width += anim::interpolate(0, _st.size - _st.shift, shown);
}
_width = width + anim::interpolate(0, _st.shift, maxShown);
if (_repaint) {
_repaint();
}
}
} // namespace Ui

View File

@@ -0,0 +1,74 @@
/*
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 "base/timer.h"
namespace style {
struct GroupCallUserpics;
} // namespace style
namespace Ui {
struct GroupCallUser {
QImage userpic;
std::pair<uint64, uint64> userpicKey = {};
uint64 id = 0;
bool speaking = false;
};
class GroupCallUserpics final {
public:
GroupCallUserpics(
const style::GroupCallUserpics &st,
rpl::producer<bool> &&hideBlobs,
Fn<void()> repaint);
~GroupCallUserpics();
void update(
const std::vector<GroupCallUser> &users,
bool visible);
void paint(QPainter &p, int x, int y, int size);
void finishAnimating();
[[nodiscard]] int maxWidth() const;
[[nodiscard]] rpl::producer<int> widthValue() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
using User = GroupCallUser;
struct BlobsAnimation;
struct Userpic;
void toggle(Userpic &userpic, bool shown);
void updatePositions();
void validateCache(Userpic &userpic);
[[nodiscard]] bool needCacheRefresh(Userpic &userpic);
void ensureBlobsAnimation(Userpic &userpic);
void sendRandomLevels();
void recountAndRepaint();
const style::GroupCallUserpics &_st;
std::vector<Userpic> _list;
base::Timer _randomSpeakingTimer;
Fn<void()> _repaint;
Ui::Animations::Basic _speakingAnimation;
int _maxWidth = 0;
bool _skipLevelUpdate = false;
crl::time _speakingAnimationHideLastTime = 0;
rpl::variable<int> _width;
rpl::lifetime _lifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,669 @@
/*
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 "ui/chat/message_bar.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/text/text_options.h"
#include "ui/ui_utility.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/palette.h"
namespace Ui {
namespace {
[[nodiscard]] int SameFirstPartLength(const QString &a, const QString &b) {
const auto &[i, j] = ranges::mismatch(a, b);
return (i - a.begin());
}
[[nodiscard]] bool MuchDifferent(
int same,
const QString &a,
const QString &b) {
return (same * 2 < a.size()) || (same * 2 < b.size());
}
[[nodiscard]] bool MuchDifferent(const QString &a, const QString &b) {
return MuchDifferent(SameFirstPartLength(a, b), a, b);
}
[[nodiscard]] bool ComplexTitleAnimation(
int same,
const QString &a,
const QString &b) {
return !MuchDifferent(same, a, b)
&& (same != a.size() || same != b.size());
}
} // namespace
MessageBar::MessageBar(
not_null<QWidget*> parent,
const style::MessageBar &st,
Fn<bool()> customEmojiPaused)
: _st(st)
, _widget(parent)
, _customEmojiPaused(std::move(customEmojiPaused)) {
setup();
style::PaletteChanged(
) | rpl::on_next([=] {
_topBarGradient = _bottomBarGradient = QPixmap();
}, _widget.lifetime());
}
void MessageBar::customEmojiRepaint() {
if (_customEmojiRepaintScheduled) {
return;
}
_customEmojiRepaintScheduled = true;
_widget.update();
}
void MessageBar::setup() {
_widget.resize(0, st::historyReplyHeight);
_widget.paintRequest(
) | rpl::on_next([=](QRect rect) {
auto p = Painter(&_widget);
p.setInactive(_customEmojiPaused());
_customEmojiRepaintScheduled = false;
paint(p);
}, _widget.lifetime());
}
void MessageBar::set(MessageBarContent &&content) {
_contentLifetime.destroy();
tweenTo(std::move(content));
}
void MessageBar::set(rpl::producer<MessageBarContent> content) {
_contentLifetime.destroy();
std::move(
content
) | rpl::on_next([=](MessageBarContent &&content) {
tweenTo(std::move(content));
}, _contentLifetime);
}
MessageBar::BodyAnimation MessageBar::DetectBodyAnimationType(
Animation *currentAnimation,
const MessageBarContent &currentContent,
const MessageBarContent &nextContent) {
const auto now = currentAnimation
? currentAnimation->bodyAnimation
: BodyAnimation::None;
const auto somethingChanged = (currentContent.text != nextContent.text)
|| (currentContent.title != nextContent.title)
|| (currentContent.index != nextContent.index)
|| (currentContent.count != nextContent.count);
return (now == BodyAnimation::Full
|| MuchDifferent(currentContent.title, nextContent.title)
|| (currentContent.title.isEmpty() && somethingChanged))
? BodyAnimation::Full
: (now == BodyAnimation::Text || somethingChanged)
? BodyAnimation::Text
: BodyAnimation::None;
}
void MessageBar::tweenTo(MessageBarContent &&content) {
Expects(content.count > 0);
Expects(content.index >= 0 && content.index < content.count);
_widget.update();
if (!_st.duration || anim::Disabled() || _widget.size().isEmpty()) {
updateFromContent(std::move(content));
return;
}
const auto hasImageChanged = (_content.preview.isNull()
!= content.preview.isNull());
const auto bodyChanged = (_content.index != content.index
|| _content.count != content.count
|| _content.title != content.title
|| _content.text != content.text
|| _content.preview.constBits() != content.preview.constBits());
const auto barCountChanged = (_content.count != content.count);
const auto barFrom = _content.index;
const auto barTo = content.index;
auto animation = Animation();
animation.bodyAnimation = DetectBodyAnimationType(
_animation.get(),
_content,
content);
animation.movingTo = (content.index > _content.index)
? RectPart::Top
: (content.index < _content.index)
? RectPart::Bottom
: RectPart::None;
animation.imageFrom = grabImagePart();
animation.spoilerFrom = std::move(_spoiler);
animation.bodyOrTextFrom = grabBodyOrTextPart(animation.bodyAnimation);
const auto sameLength = SameFirstPartLength(
_content.title,
content.title);
if (animation.bodyAnimation == BodyAnimation::Text
&& ComplexTitleAnimation(sameLength, _content.title, content.title)) {
animation.titleSame = grabTitleBase(sameLength);
animation.titleFrom = grabTitlePart(sameLength);
}
auto was = std::move(_animation);
updateFromContent(std::move(content));
animation.imageTo = grabImagePart();
animation.bodyOrTextTo = grabBodyOrTextPart(animation.bodyAnimation);
if (!animation.titleSame.isNull()) {
animation.titleTo = grabTitlePart(sameLength);
}
if (was) {
_animation = std::move(was);
std::swap(*_animation, animation);
_animation->imageShown = std::move(animation.imageShown);
_animation->barScroll = std::move(animation.barScroll);
_animation->barTop = std::move(animation.barTop);
} else {
_animation = std::make_unique<Animation>(std::move(animation));
}
if (hasImageChanged) {
_animation->imageShown.start(
[=] { _widget.update(); },
_image.isNull() ? 1. : 0.,
_image.isNull() ? 0. : 1.,
_st.duration);
}
if (bodyChanged) {
_animation->bodyMoved.start(
[=] { _widget.update(); },
0.,
1.,
_st.duration);
}
if (barCountChanged) {
_animation->barScroll.stop();
_animation->barTop.stop();
} else if (barFrom != barTo) {
const auto wasState = countBarState(barFrom);
const auto nowState = countBarState(barTo);
_animation->barScroll.start(
[=] { _widget.update(); },
wasState.scroll,
nowState.scroll,
_st.duration);
_animation->barTop.start(
[] {},
wasState.offset,
nowState.offset,
_st.duration);
}
}
void MessageBar::updateFromContent(MessageBarContent &&content) {
_content = std::move(content);
_title.setText(_st.title, _content.title);
_text.setMarkedText(
_st.text,
_content.text,
Ui::DialogTextOptions(),
_content.context);
_image = prepareImage(_content.preview);
if (!_content.spoilerRepaint) {
_spoiler = nullptr;
} else if (!_spoiler) {
_spoiler = std::make_unique<SpoilerAnimation>(
_content.spoilerRepaint);
}
}
QRect MessageBar::imageRect() const {
const auto left = st::msgReplyBarSkip + st::msgReplyBarSkip;
const auto top = (st::historyReplyHeight - st::historyReplyPreview) / 2;
const auto size = st::historyReplyPreview;
return QRect(left, top, size, size);
}
QRect MessageBar::titleRangeRect(int from, int till) const {
auto result = bodyRect();
result.setHeight(st::msgServiceNameFont->height);
const auto left = from
? st::msgServiceNameFont->width(_content.title.mid(0, from))
: 0;
const auto right = (till <= _content.title.size())
? st::msgServiceNameFont->width(_content.title.mid(0, till))
: result.width();
result.setLeft(result.left() + left);
result.setWidth(right - left);
return result;
}
QRect MessageBar::bodyRect(bool withImage) const {
const auto innerLeft = st::msgReplyBarSkip + st::msgReplyBarSkip;
const auto imageSkip = st::historyReplyPreview + st::msgReplyBarSkip;
const auto left = innerLeft + (withImage ? imageSkip : 0);
const auto top = st::msgReplyPadding.top();
const auto width = _widget.width() - left - st::msgReplyPadding.right();
const auto height = (st::historyReplyHeight - 2 * top);
return QRect(left, top, width, height) - _content.margins;
}
QRect MessageBar::bodyRect() const {
return bodyRect(!_image.isNull());
}
QRect MessageBar::textRect() const {
auto result = bodyRect();
result.setTop(result.top() + st::msgServiceNameFont->height);
return result;
}
auto MessageBar::makeGrabGuard() {
auto imageShown = _animation
? std::move(_animation->imageShown)
: Ui::Animations::Simple();
auto spoiler = std::move(_spoiler);
auto fromSpoiler = _animation
? std::move(_animation->spoilerFrom)
: nullptr;
return gsl::finally([
&,
shown = std::move(imageShown),
spoiler = std::move(spoiler),
fromSpoiler = std::move(fromSpoiler)
]() mutable {
if (_animation) {
_animation->imageShown = std::move(shown);
_animation->spoilerFrom = std::move(fromSpoiler);
}
_spoiler = std::move(spoiler);
});
}
QPixmap MessageBar::grabBodyOrTextPart(BodyAnimation type) {
return (type == BodyAnimation::Full)
? grabBodyPart()
: (type == BodyAnimation::Text)
? grabTextPart()
: QPixmap();
}
QPixmap MessageBar::grabTitleBase(int till) {
return grabTitleRange(0, till);
}
QPixmap MessageBar::grabTitlePart(int from) {
Expects(from <= _content.title.size());
return grabTitleRange(from, _content.title.size());
}
QPixmap MessageBar::grabTitleRange(int from, int till) {
const auto guard = makeGrabGuard();
return GrabWidget(widget(), titleRangeRect(from, till));
}
QPixmap MessageBar::grabBodyPart() {
const auto guard = makeGrabGuard();
return GrabWidget(widget(), bodyRect());
}
QPixmap MessageBar::grabTextPart() {
const auto guard = makeGrabGuard();
return GrabWidget(widget(), textRect());
}
QPixmap MessageBar::grabImagePart() {
if (!_animation) {
return _image;
}
const auto guard = makeGrabGuard();
return (_animation->bodyMoved.animating()
&& !_animation->imageFrom.isNull()
&& !_animation->imageTo.isNull())
? GrabWidget(widget(), imageRect())
: _animation->imageFrom;
}
void MessageBar::finishAnimating() {
if (_animation) {
_animation = nullptr;
_widget.update();
}
}
QPixmap MessageBar::prepareImage(const QImage &preview) {
return QPixmap::fromImage(preview, Qt::ColorOnly);
}
void MessageBar::paint(Painter &p) {
const auto progress = _animation ? _animation->bodyMoved.value(1.) : 1.;
const auto imageFinal = _image.isNull() ? 0. : 1.;
const auto imageShown = _animation
? _animation->imageShown.value(imageFinal)
: imageFinal;
if (progress == 1. && imageShown == imageFinal && _animation) {
_animation = nullptr;
}
const auto body = [&] {
if (!_animation || !_animation->imageShown.animating()) {
return bodyRect();
}
const auto noImage = bodyRect(false);
const auto withImage = bodyRect(true);
return QRect(
anim::interpolate(noImage.x(), withImage.x(), imageShown),
noImage.y(),
anim::interpolate(noImage.width(), withImage.width(), imageShown),
noImage.height());
}();
const auto text = textRect();
const auto image = imageRect();
const auto width = _widget.width();
const auto noShift = !_animation
|| (_animation->movingTo == RectPart::None);
const auto shiftFull = st::msgReplyBarSkip;
const auto shiftTo = noShift
? 0
: (_animation->movingTo == RectPart::Top)
? anim::interpolate(shiftFull, 0, progress)
: anim::interpolate(-shiftFull, 0, progress);
const auto shiftFrom = noShift
? 0
: (_animation->movingTo == RectPart::Top)
? (shiftTo - shiftFull)
: (shiftTo + shiftFull);
const auto now = crl::now();
const auto paused = p.inactive();
const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);
paintLeftBar(p);
if (!_animation) {
if (!_image.isNull()) {
paintImageWithSpoiler(
p,
image,
_image,
_spoiler.get(),
now,
pausedSpoiler);
}
} else if (!_animation->imageTo.isNull()
|| (!_animation->imageFrom.isNull()
&& _animation->imageShown.animating())) {
const auto rect = [&] {
if (!_animation->imageShown.animating()) {
return image;
}
const auto size = anim::interpolate(0, image.width(), imageShown);
return QRect(
image.x(),
image.y() + (image.height() - size) / 2,
size,
size);
}();
if (_animation->bodyMoved.animating()) {
p.setOpacity(1. - progress);
paintImageWithSpoiler(
p,
rect.translated(0, shiftFrom),
_animation->imageFrom,
_animation->spoilerFrom.get(),
now,
pausedSpoiler);
p.setOpacity(progress);
paintImageWithSpoiler(
p,
rect.translated(0, shiftTo),
_animation->imageTo,
_spoiler.get(),
now,
pausedSpoiler);
p.setOpacity(1.);
} else {
paintImageWithSpoiler(
p,
rect,
_image,
_spoiler.get(),
now,
pausedSpoiler);
}
}
if (!_animation || _animation->bodyAnimation == BodyAnimation::None) {
if (_title.isEmpty()) {
// "Loading..." state.
p.setPen(st::historyComposeAreaFgService);
_text.draw(p, {
.position = {
body.x(),
body.y() + (body.height() - st::normalFont->height) / 2,
},
.outerWidth = width,
.availableWidth = body.width(),
.elisionLines = 1,
});
} else {
p.setPen(_st.textFg);
_text.draw(p, {
.position = { body.x(), text.y() },
.outerWidth = width,
.availableWidth = body.width(),
.palette = &_st.textPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = now,
.pausedEmoji = paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = pausedSpoiler,
.elisionLines = 1,
});
}
} else if (_animation->bodyAnimation == BodyAnimation::Text) {
p.setOpacity(1. - progress);
p.drawPixmap(
body.x(),
text.y() + shiftFrom,
_animation->bodyOrTextFrom);
p.setOpacity(progress);
p.drawPixmap(body.x(), text.y() + shiftTo, _animation->bodyOrTextTo);
p.setOpacity(1.);
}
if (!_animation || _animation->bodyAnimation != BodyAnimation::Full) {
if (_animation && !_animation->titleSame.isNull()) {
const auto factor = style::DevicePixelRatio();
p.drawPixmap(body.x(), body.y(), _animation->titleSame);
p.setOpacity(1. - progress);
p.drawPixmap(
body.x() + (_animation->titleSame.width() / factor),
body.y() + shiftFrom,
_animation->titleFrom);
p.setOpacity(progress);
p.drawPixmap(
body.x() + (_animation->titleSame.width() / factor),
body.y() + shiftTo,
_animation->titleTo);
p.setOpacity(1.);
} else {
p.setPen(_st.titleFg);
_title.drawLeftElided(p, body.x(), body.y(), body.width(), width);
}
} else {
p.setOpacity(1. - progress);
p.drawPixmap(
body.x(),
body.y() + shiftFrom,
_animation->bodyOrTextFrom);
p.setOpacity(progress);
p.drawPixmap(body.x(), body.y() + shiftTo, _animation->bodyOrTextTo);
p.setOpacity(1.);
}
}
auto MessageBar::countBarState(int index) const -> BarState {
Expects(index >= 0 && index < _content.count);
auto result = BarState();
const auto line = st::msgReplyBarSize.width();
const auto height = st::msgReplyBarSize.height();
const auto count = _content.count;
const auto shownCount = std::min(count, 4);
const auto dividers = (shownCount - 1) * line;
const auto size = float64(st::msgReplyBarSize.height() - dividers)
/ shownCount;
const auto fullHeight = count * size + (count - 1) * line;
const auto topByIndex = [&](int index) {
return index * (size + line);
};
result.scroll = (count < 5 || index < 2)
? 0
: (index >= count - 2)
? (fullHeight - height)
: (topByIndex(index) - (height - size) / 2);
result.size = size;
result.skip = line;
result.offset = topByIndex(index);
return result;
}
auto MessageBar::countBarState() const -> BarState {
return countBarState(_content.index);
}
void MessageBar::ensureGradientsCreated(int size) {
if (!_topBarGradient.isNull()) {
return;
}
const auto rows = size * style::DevicePixelRatio() - 2;
auto bottomMask = QImage(
QSize(1, size) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
const auto step = ((1ULL << 24) - 1) / rows;
const auto limit = step * rows;
auto bits = bottomMask.bits();
const auto perLine = bottomMask.bytesPerLine();
for (auto counter = uint32(0); counter != limit; counter += step) {
const auto value = (counter >> 16);
memset(bits, int(value), perLine);
bits += perLine;
}
memset(bits, 255, perLine * 2);
auto bottom = style::colorizeImage(bottomMask, st::historyPinnedBg);
bottom.setDevicePixelRatio(style::DevicePixelRatio());
auto top = bottom.mirrored();
_bottomBarGradient = Images::PixmapFast(std::move(bottom));
_topBarGradient = Images::PixmapFast(std::move(top));
}
void MessageBar::paintImageWithSpoiler(
QPainter &p,
QRect rect,
const QPixmap &image,
SpoilerAnimation *spoiler,
crl::time now,
bool paused) const {
p.drawPixmap(rect, image);
if (spoiler) {
const auto frame = DefaultImageSpoiler().frame(
spoiler->index(now, paused));
FillSpoilerRect(p, rect, frame);
}
}
void MessageBar::paintLeftBar(Painter &p) {
const auto state = countBarState();
const auto gradientSize = int(std::ceil(state.size * 2.5));
if (_content.count > 4) {
ensureGradientsCreated(gradientSize);
}
const auto scroll = _animation
? _animation->barScroll.value(state.scroll)
: state.scroll;
const auto offset = _animation
? _animation->barTop.value(state.offset)
: state.offset;
const auto line = st::msgReplyBarSize.width();
const auto height = st::msgReplyBarSize.height();
const auto activeFrom = offset - scroll;
const auto activeTill = activeFrom + state.size;
const auto single = state.size + state.skip;
const auto barSkip = st::msgReplyPadding.top() + st::msgReplyBarPos.y();
const auto fullHeight = barSkip + height + barSkip;
const auto bar = QRect(
st::msgReplyBarSkip + st::msgReplyBarPos.x(),
barSkip,
line,
state.size);
const auto paintFromScroll = std::max(scroll - barSkip, 0.);
const auto paintFrom = int(std::floor(paintFromScroll / single));
const auto paintTillScroll = (scroll + height + barSkip);
const auto paintTill = std::min(
int(std::floor(paintTillScroll / single)) + 1,
_content.count);
p.setPen(Qt::NoPen);
const auto activeBrush = QBrush(st::msgInReplyBarColor);
const auto inactiveBrush = QBrush(QColor(
st::msgInReplyBarColor->c.red(),
st::msgInReplyBarColor->c.green(),
st::msgInReplyBarColor->c.blue(),
st::msgInReplyBarColor->c.alpha() / 3));
const auto radius = line / 2.;
auto hq = PainterHighQualityEnabler(p);
p.setClipRect(bar.x(), 0, bar.width(), fullHeight);
for (auto i = paintFrom; i != paintTill; ++i) {
const auto top = i * single - scroll;
const auto bottom = top + state.size;
const auto active = (top == activeFrom);
p.setBrush(active ? activeBrush : inactiveBrush);
p.drawRoundedRect(bar.translated(0, top), radius, radius);
if (active
|| bottom - line <= activeFrom
|| top + line >= activeTill) {
continue;
}
const auto partFrom = std::max(top, activeFrom);
const auto partTill = std::min(bottom, activeTill);
p.setBrush(activeBrush);
p.drawRoundedRect(
QRect(bar.x(), bar.y() + partFrom, line, partTill - partFrom),
radius,
radius);
}
p.setClipping(false);
if (_content.count > 4) {
const auto firstScroll = countBarState(2).scroll;
const auto gradientTop = (scroll >= firstScroll)
? 0
: anim::interpolate(-gradientSize, 0, scroll / firstScroll);
const auto lastScroll = countBarState(_content.count - 3).scroll;
const auto largestScroll = countBarState(_content.count - 1).scroll;
const auto gradientBottom = (scroll <= lastScroll)
? fullHeight
: anim::interpolate(
fullHeight,
fullHeight + gradientSize,
(scroll - lastScroll) / (largestScroll - lastScroll));
if (gradientTop > -gradientSize) {
p.drawPixmap(
QRect(bar.x(), gradientTop, bar.width(), gradientSize),
_topBarGradient);
}
if (gradientBottom < fullHeight + gradientSize) {
p.drawPixmap(
QRect(
bar.x(),
gradientBottom - gradientSize,
bar.width(),
gradientSize),
_bottomBarGradient);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,132 @@
/*
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 "ui/effects/animations.h"
#include "ui/rect_part.h"
#include "ui/rp_widget.h"
class Painter;
namespace style {
struct MessageBar;
} // namespace style
namespace Ui {
class SpoilerAnimation;
struct MessageBarContent {
int index = 0;
int count = 1;
QString title;
TextWithEntities text;
Text::MarkedContext context;
QImage preview;
Fn<void()> spoilerRepaint;
style::margins margins;
};
class MessageBar final {
public:
MessageBar(
not_null<QWidget*> parent,
const style::MessageBar &st,
Fn<bool()> customEmojiPaused);
void set(MessageBarContent &&content);
void set(rpl::producer<MessageBarContent> content);
[[nodiscard]] not_null<RpWidget*> widget() {
return &_widget;
}
void customEmojiRepaint();
void finishAnimating();
private:
enum class BodyAnimation : char {
Full,
Text,
None,
};
struct Animation {
Animations::Simple bodyMoved;
Animations::Simple imageShown;
Animations::Simple barScroll;
Animations::Simple barTop;
QPixmap bodyOrTextFrom;
QPixmap bodyOrTextTo;
QPixmap titleSame;
QPixmap titleFrom;
QPixmap titleTo;
QPixmap imageFrom;
QPixmap imageTo;
std::unique_ptr<SpoilerAnimation> spoilerFrom;
BodyAnimation bodyAnimation = BodyAnimation::None;
RectPart movingTo = RectPart::None;
};
struct BarState {
float64 scroll = 0.;
float64 size = 0.;
float64 skip = 0.;
float64 offset = 0.;
};
void setup();
void paint(Painter &p);
void paintLeftBar(Painter &p);
void tweenTo(MessageBarContent &&content);
void updateFromContent(MessageBarContent &&content);
[[nodiscard]] QPixmap prepareImage(const QImage &preview);
[[nodiscard]] QRect imageRect() const;
[[nodiscard]] QRect titleRangeRect(int from, int till) const;
[[nodiscard]] QRect bodyRect(bool withImage) const;
[[nodiscard]] QRect bodyRect() const;
[[nodiscard]] QRect textRect() const;
auto makeGrabGuard();
[[nodiscard]] QPixmap grabBodyOrTextPart(BodyAnimation type);
[[nodiscard]] QPixmap grabTitleBase(int till);
[[nodiscard]] QPixmap grabTitlePart(int from);
[[nodiscard]] QPixmap grabTitleRange(int from, int till);
[[nodiscard]] QPixmap grabImagePart();
[[nodiscard]] QPixmap grabBodyPart();
[[nodiscard]] QPixmap grabTextPart();
[[nodiscard]] BarState countBarState(int index) const;
[[nodiscard]] BarState countBarState() const;
void ensureGradientsCreated(int size);
void paintImageWithSpoiler(
QPainter &p,
QRect rect,
const QPixmap &image,
SpoilerAnimation *spoiler,
crl::time now,
bool paused) const;
[[nodiscard]] static BodyAnimation DetectBodyAnimationType(
Animation *currentAnimation,
const MessageBarContent &currentContent,
const MessageBarContent &nextContent);
const style::MessageBar &_st;
RpWidget _widget;
Fn<bool()> _customEmojiPaused;
MessageBarContent _content;
rpl::lifetime _contentLifetime;
Text::String _title, _text;
QPixmap _image, _topBarGradient, _bottomBarGradient;
std::unique_ptr<Animation> _animation;
std::unique_ptr<SpoilerAnimation> _spoiler;
bool _customEmojiRepaintScheduled = false;
};
} // namespace Ui

View File

@@ -0,0 +1,461 @@
/*
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 "ui/chat/message_bubble.h"
#include "ui/cached_round_corners.h"
#include "ui/image/image_prepare.h"
#include "ui/chat/chat_style.h"
#include "styles/style_chat.h"
namespace Ui {
namespace {
using Corner = BubbleCornerRounding;
template <
typename FillBg, // fillBg(QRect rect)
typename FillSh, // fillSh(QRect rect)
typename FillCorner, // fillCorner(int x, int y, int index, Corner size)
typename PaintTail> // paintTail(QPoint bottomPosition) -> tailWidth
void PaintBubbleGeneric(
const SimpleBubble &args,
FillBg &&fillBg,
FillSh &&fillSh,
FillCorner &&fillCorner,
PaintTail &&paintTail) {
using namespace Images;
const auto topLeft = args.rounding.topLeft;
const auto topRight = args.rounding.topRight;
const auto bottomWithTailLeft = args.rounding.bottomLeft;
const auto bottomWithTailRight = args.rounding.bottomRight;
if (topLeft == Corner::None
&& topRight == Corner::None
&& bottomWithTailLeft == Corner::None
&& bottomWithTailRight == Corner::None) {
fillBg(args.geometry);
return;
}
const auto bottomLeft = (bottomWithTailLeft == Corner::Tail)
? Corner::None
: bottomWithTailLeft;
const auto bottomRight = (bottomWithTailRight == Corner::Tail)
? Corner::None
: bottomWithTailRight;
const auto rect = args.geometry;
const auto small = BubbleRadiusSmall();
const auto large = BubbleRadiusLarge();
const auto cornerSize = [&](Corner corner) {
return (corner == Corner::Large)
? large
: (corner == Corner::Small)
? small
: 0;
};
const auto verticalSkip = [&](Corner left, Corner right) {
return std::max(cornerSize(left), cornerSize(right));
};
const auto top = verticalSkip(topLeft, topRight);
const auto bottom = verticalSkip(bottomLeft, bottomRight);
if (top) {
const auto left = cornerSize(topLeft);
const auto right = cornerSize(topRight);
if (left) {
fillCorner(rect.left(), rect.top(), kTopLeft, topLeft);
if (const auto add = top - left) {
fillBg({ rect.left(), rect.top() + left, left, add });
}
}
if (const auto fill = rect.width() - left - right; fill > 0) {
fillBg({ rect.left() + left, rect.top(), fill, top });
}
if (right) {
fillCorner(
rect.left() + rect.width() - right,
rect.top(),
kTopRight,
topRight);
if (const auto add = top - right) {
fillBg({
rect.left() + rect.width() - right,
rect.top() + right,
right,
add,
});
}
}
}
if (const auto fill = rect.height() - top - bottom; fill > 0) {
fillBg({ rect.left(), rect.top() + top, rect.width(), fill });
}
if (bottom) {
const auto left = cornerSize(bottomLeft);
const auto right = cornerSize(bottomRight);
if (left) {
fillCorner(
rect.left(),
rect.top() + rect.height() - left,
kBottomLeft,
bottomLeft);
if (const auto add = bottom - left) {
fillBg({
rect.left(),
rect.top() + rect.height() - bottom,
left,
add,
});
}
}
if (const auto fill = rect.width() - left - right; fill > 0) {
fillBg({
rect.left() + left,
rect.top() + rect.height() - bottom,
fill,
bottom,
});
}
if (right) {
fillCorner(
rect.left() + rect.width() - right,
rect.top() + rect.height() - right,
kBottomRight,
bottomRight);
if (const auto add = bottom - right) {
fillBg({
rect.left() + rect.width() - right,
rect.top() + rect.height() - bottom,
right,
add,
});
}
}
}
const auto leftTail = (bottomWithTailLeft == Corner::Tail)
? paintTail({ rect.x(), rect.y() + rect.height() })
: 0;
const auto rightTail = (bottomWithTailRight == Corner::Tail)
? paintTail({ rect.x() + rect.width(), rect.y() + rect.height() })
: 0;
if (!args.shadowed) {
return;
}
const auto shLeft = rect.x() + cornerSize(bottomLeft) - leftTail;
const auto shWidth = rect.x()
+ rect.width()
- cornerSize(bottomRight)
+ rightTail
- shLeft;
if (shWidth > 0) {
fillSh({ shLeft, rect.y() + rect.height(), shWidth, st::msgShadow });
}
}
void PaintPatternBubble(QPainter &p, const SimpleBubble &args) {
const auto wasOpacity = p.opacity();
const auto opacity = args.st->msgOutBg()->c.alphaF() * wasOpacity;
const auto shadowOpacity = opacity * args.st->msgOutShadow()->c.alphaF();
const auto pattern = args.pattern;
const auto &tail = (args.rounding.bottomRight == Corner::Tail)
? pattern->tailRight
: pattern->tailLeft;
const auto tailShift = (args.rounding.bottomRight == Corner::Tail
? QPoint(0, tail.height())
: QPoint(tail.width(), tail.height())) / int(tail.devicePixelRatio());
const auto fillBg = [&](const QRect &rect) {
const auto fill = rect.intersected(args.patternViewport);
if (!fill.isEmpty()) {
PaintPatternBubblePart(
p,
args.patternViewport,
pattern->pixmap,
fill);
}
};
const auto fillSh = [&](const QRect &rect) {
p.setOpacity(shadowOpacity);
fillBg(rect);
p.setOpacity(opacity);
};
const auto fillPattern = [&](
int x,
int y,
const QImage &mask,
QImage &cache) {
PaintPatternBubblePart(
p,
args.patternViewport,
pattern->pixmap,
QRect(QPoint(x, y), mask.size() / int(mask.devicePixelRatio())),
mask,
cache);
};
const auto fillCorner = [&](int x, int y, int index, Corner size) {
auto &corner = (size == Corner::Large)
? pattern->cornersLarge[index]
: pattern->cornersSmall[index];
auto &cache = (size == Corner::Large)
? (index < 2
? pattern->cornerTopLargeCache
: pattern->cornerBottomLargeCache)
: (index < 2
? pattern->cornerTopSmallCache
: pattern->cornerBottomSmallCache);
fillPattern(x, y, corner, cache);
};
const auto paintTail = [&](QPoint bottomPosition) {
const auto position = bottomPosition - tailShift;
fillPattern(position.x(), position.y(), tail, pattern->tailCache);
return tail.width() / int(tail.devicePixelRatio());
};
p.setOpacity(opacity);
PaintBubbleGeneric(args, fillBg, fillSh, fillCorner, paintTail);
p.setOpacity(wasOpacity);
}
void PaintSolidBubble(QPainter &p, const SimpleBubble &args) {
const auto &st = args.st->messageStyle(args.outbg, args.selected);
const auto &bg = st.msgBg;
const auto sh = (args.rounding.bottomRight == Corner::None)
? nullptr
: &st.msgShadow;
const auto &tail = (args.rounding.bottomRight == Corner::Tail)
? st.tailRight
: st.tailLeft;
const auto tailShift = (args.rounding.bottomRight == Corner::Tail)
? QPoint(0, tail.height())
: QPoint(tail.width(), tail.height());
PaintBubbleGeneric(args, [&](const QRect &rect) {
p.fillRect(rect, bg);
}, [&](const QRect &rect) {
p.fillRect(rect, *sh);
}, [&](int x, int y, int index, Corner size) {
auto &corners = (size == Corner::Large)
? st.msgBgCornersLarge
: st.msgBgCornersSmall;
p.drawPixmap(x, y, corners.p[index]);
}, [&](const QPoint &bottomPosition) {
tail.paint(p, bottomPosition - tailShift, args.outerWidth);
return tail.width();
});
}
} // namespace
std::unique_ptr<BubblePattern> PrepareBubblePattern(
not_null<const style::palette*> st) {
auto result = std::make_unique<Ui::BubblePattern>();
result->cornersSmall = Images::CornersMask(BubbleRadiusSmall());
result->cornersLarge = Images::CornersMask(BubbleRadiusLarge());
const auto addShadow = [&](QImage &bottomCorner) {
auto result = QImage(
bottomCorner.width(),
(bottomCorner.height()
+ st::msgShadow * int(bottomCorner.devicePixelRatio())),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(bottomCorner.devicePixelRatio());
auto p = QPainter(&result);
p.setOpacity(st->msgInShadow()->c.alphaF());
p.drawImage(0, st::msgShadow, bottomCorner);
p.setOpacity(1.);
p.drawImage(0, 0, bottomCorner);
p.end();
bottomCorner = std::move(result);
};
addShadow(result->cornersSmall[2]);
addShadow(result->cornersSmall[3]);
result->cornerTopSmallCache = QImage(
result->cornersSmall[0].size(),
QImage::Format_ARGB32_Premultiplied);
result->cornerTopLargeCache = QImage(
result->cornersLarge[0].size(),
QImage::Format_ARGB32_Premultiplied);
result->cornerBottomSmallCache = QImage(
result->cornersSmall[2].size(),
QImage::Format_ARGB32_Premultiplied);
result->cornerBottomLargeCache = QImage(
result->cornersLarge[2].size(),
QImage::Format_ARGB32_Premultiplied);
return result;
}
void FinishBubblePatternOnMain(not_null<BubblePattern*> pattern) {
pattern->tailLeft = st::historyBubbleTailOutLeft.instance(Qt::white);
pattern->tailRight = st::historyBubbleTailOutRight.instance(Qt::white);
pattern->tailCache = QImage(
pattern->tailLeft.size(),
QImage::Format_ARGB32_Premultiplied);
}
void PaintBubble(QPainter &p, const SimpleBubble &args) {
if (!args.selected
&& args.outbg
&& args.pattern
&& !args.patternViewport.isEmpty()
&& !args.pattern->pixmap.size().isEmpty()) {
PaintPatternBubble(p, args);
} else {
PaintSolidBubble(p, args);
}
}
void PaintBubble(QPainter &p, const ComplexBubble &args) {
if (args.selection.empty()) {
PaintBubble(p, args.simple);
return;
}
const auto rect = args.simple.geometry;
const auto left = rect.x();
const auto width = rect.width();
const auto top = rect.y();
const auto bottom = top + rect.height();
const auto paintOne = [&](
QRect geometry,
bool selected,
bool fromTop,
bool tillBottom) {
auto simple = args.simple;
simple.geometry = geometry;
simple.selected = selected;
if (!fromTop) {
simple.rounding.topLeft
= simple.rounding.topRight
= Corner::None;
}
if (!tillBottom) {
simple.rounding.bottomLeft
= simple.rounding.bottomRight
= Corner::None;
simple.shadowed = false;
}
PaintBubble(p, simple);
};
auto from = top;
for (const auto &selected : args.selection) {
if (selected.top > from) {
paintOne(
QRect(left, from, width, selected.top - from),
false,
(from <= top),
false);
}
paintOne(
QRect(left, selected.top, width, selected.height),
true,
(selected.top <= top),
(selected.top + selected.height >= bottom));
from = selected.top + selected.height;
}
if (from < bottom) {
paintOne(
QRect(left, from, width, bottom - from),
false,
false,
true);
}
}
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target) {
const auto factor = pixmap.devicePixelRatio();
if (viewport.size() * factor == pixmap.size()) {
const auto fill = target.intersected(viewport);
if (fill.isEmpty()) {
return;
}
p.drawPixmap(fill, pixmap, QRect(
(fill.topLeft() - viewport.topLeft()) * factor,
fill.size() * factor));
} else {
const auto to = viewport;
const auto from = QRect(QPoint(), pixmap.size());
const auto deviceRect = QRect(
QPoint(),
QSize(p.device()->width(), p.device()->height()));
const auto clip = (target != deviceRect);
if (clip) {
p.setClipRect(target);
}
p.drawPixmap(to, pixmap, from);
if (clip) {
p.setClipping(false);
}
}
}
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target,
const QImage &mask,
QImage &cache) {
Expects(mask.bytesPerLine() == mask.width() * 4);
Expects(mask.format() == QImage::Format_ARGB32_Premultiplied);
if (cache.size() != mask.size()) {
cache = QImage(
mask.size(),
QImage::Format_ARGB32_Premultiplied);
}
cache.setDevicePixelRatio(mask.devicePixelRatio());
Assert(cache.bytesPerLine() == cache.width() * 4);
memcpy(cache.bits(), mask.constBits(), mask.sizeInBytes());
auto q = QPainter(&cache);
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
PaintPatternBubblePart(
q,
viewport.translated(-target.topLeft()),
pixmap,
QRect(QPoint(), cache.size() / int(cache.devicePixelRatio())));
q.end();
p.drawImage(target, cache);
}
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target,
Fn<void(QPainter&)> paintContent,
QImage &cache) {
Expects(paintContent != nullptr);
const auto targetOrigin = target.topLeft();
const auto targetSize = target.size();
if (cache.size() != targetSize * style::DevicePixelRatio()) {
cache = QImage(
target.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(style::DevicePixelRatio());
}
cache.fill(Qt::transparent);
auto q = QPainter(&cache);
q.translate(-targetOrigin);
paintContent(q);
q.translate(targetOrigin);
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
PaintPatternBubblePart(
q,
viewport.translated(-targetOrigin),
pixmap,
QRect(QPoint(), targetSize));
q.end();
p.drawImage(target, cache);
}
} // namespace Ui

View File

@@ -0,0 +1,149 @@
/*
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
namespace Ui {
class ChatTheme;
class ChatStyle;
enum class BubbleCornerRounding : uchar {
None,
Tail,
Small,
Large,
};
struct BubbleRounding {
BubbleCornerRounding topLeft : 2 = BubbleCornerRounding();
BubbleCornerRounding topRight : 2 = BubbleCornerRounding();
BubbleCornerRounding bottomLeft : 2 = BubbleCornerRounding();
BubbleCornerRounding bottomRight : 2 = BubbleCornerRounding();
struct ConstProxy {
constexpr ConstProxy(
not_null<const BubbleRounding*> that,
int index) noexcept
: that(that)
, index(index) {
Expects(index >= 0 && index < 4);
}
constexpr operator BubbleCornerRounding() const noexcept {
switch (index) {
case 0: return that->topLeft;
case 1: return that->topRight;
case 2: return that->bottomLeft;
case 3: return that->bottomRight;
}
Unexpected("Index value in BubbleRounding::ConstProxy.");
}
not_null<const BubbleRounding*> that;
int index = 0;
};
struct Proxy : ConstProxy {
constexpr Proxy(not_null<BubbleRounding*> that, int index) noexcept
: ConstProxy(that, index) {
}
using ConstProxy::operator BubbleCornerRounding;
constexpr Proxy &operator=(BubbleCornerRounding value) noexcept {
const auto nonconst = const_cast<BubbleRounding*>(that.get());
switch (index) {
case 0: nonconst->topLeft = value; break;
case 1: nonconst->topRight = value; break;
case 2: nonconst->bottomLeft = value; break;
case 3: nonconst->bottomRight = value; break;
}
return *this;
}
};
[[nodiscard]] constexpr ConstProxy operator[](int index) const {
return { this, index };
}
[[nodiscard]] constexpr Proxy operator[](int index) {
return { this, index };
}
[[nodiscard]] uchar key() const {
static_assert(sizeof(*this) == sizeof(uchar));
return uchar(*reinterpret_cast<const std::byte*>(this));
}
inline friend constexpr auto operator<=>(
BubbleRounding,
BubbleRounding) = default;
};
struct BubbleSelectionInterval {
int top = 0;
int height = 0;
};
struct BubblePattern {
QPixmap pixmap;
std::array<QImage, 4> cornersSmall;
std::array<QImage, 4> cornersLarge;
QImage tailLeft;
QImage tailRight;
mutable QImage cornerTopSmallCache;
mutable QImage cornerTopLargeCache;
mutable QImage cornerBottomSmallCache;
mutable QImage cornerBottomLargeCache;
mutable QImage tailCache;
};
[[nodiscard]] std::unique_ptr<BubblePattern> PrepareBubblePattern(
not_null<const style::palette*> st);
void FinishBubblePatternOnMain(not_null<BubblePattern*> pattern);
struct SimpleBubble {
not_null<const ChatStyle*> st;
QRect geometry;
const BubblePattern *pattern = nullptr;
QRect patternViewport;
int outerWidth = 0;
bool selected = false;
bool shadowed = true;
bool outbg = false;
BubbleRounding rounding;
};
struct ComplexBubble {
SimpleBubble simple;
const std::vector<BubbleSelectionInterval> &selection;
};
void PaintBubble(QPainter &p, const SimpleBubble &args);
void PaintBubble(QPainter &p, const ComplexBubble &args);
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target);
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target,
const QImage &mask,
QImage &cache);
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target,
Fn<void(QPainter&)> paintContent,
QImage &cache);
} // namespace Ui

View File

@@ -0,0 +1,223 @@
/*
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 "ui/chat/more_chats_bar.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/text/text_options.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h" // st::columnMinimalWidthLeft
namespace Ui {
MoreChatsBar::MoreChatsBar(
not_null<QWidget*> parent,
rpl::producer<MoreChatsBarContent> content)
: _wrap(parent, object_ptr<RpWidget>(parent))
, _inner(_wrap.entity())
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
, _close(_inner.get(), st::moreChatsBarClose) {
_wrap.hide(anim::type::instant);
_shadow->hide();
_wrap.entity()->paintRequest(
) | rpl::on_next([=](QRect clip) {
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
}, lifetime());
_wrap.setAttribute(Qt::WA_OpaquePaintEvent);
auto copy = std::move(
content
) | rpl::start_spawning(_wrap.lifetime());
rpl::duplicate(
copy
) | rpl::on_next([=](MoreChatsBarContent &&content) {
_content = content;
if (_content.count > 0) {
_text.setText(
st::defaultMessageBar.title,
tr::lng_filters_bar_you_can(
tr::now,
lt_count,
_content.count),
Ui::NameTextOptions());
_status.setText(
st::defaultMessageBar.text,
tr::lng_filters_bar_view(
tr::now,
lt_count,
_content.count),
Ui::NameTextOptions());
}
_inner->update();
}, lifetime());
std::move(
copy
) | rpl::map([=](const MoreChatsBarContent &content) {
return !content.count;
}) | rpl::on_next_done([=](bool hidden) {
_shouldBeShown = !hidden;
if (!_forceHidden) {
_wrap.toggle(_shouldBeShown, anim::type::normal);
}
}, [=] {
_forceHidden = true;
_wrap.toggle(false, anim::type::normal);
}, lifetime());
setupInner();
}
MoreChatsBar::~MoreChatsBar() = default;
void MoreChatsBar::setupInner() {
_inner->resize(0, st::moreChatsBarHeight);
_inner->paintRequest(
) | rpl::on_next([=](QRect rect) {
auto p = Painter(_inner);
paint(p);
}, _inner->lifetime());
// Clicks.
_inner->setCursor(style::cur_pointer);
_inner->events(
) | rpl::filter([=](not_null<QEvent*> event) {
return (event->type() == QEvent::MouseButtonPress);
}) | rpl::map([=] {
return _inner->events(
) | rpl::filter([=](not_null<QEvent*> event) {
return (event->type() == QEvent::MouseButtonRelease);
}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {
return _inner->rect().contains(
static_cast<QMouseEvent*>(event.get())->pos());
});
}) | rpl::flatten_latest(
) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());
_wrap.geometryValue(
) | rpl::on_next([=](QRect rect) {
updateShadowGeometry(rect);
updateControlsGeometry(rect);
}, _inner->lifetime());
}
void MoreChatsBar::paint(Painter &p) {
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
const auto width = std::max(
_inner->width(),
st::columnMinimalWidthLeft);
const auto available = width
- st::moreChatsBarTextPosition.x()
- st::moreChatsBarClose.width;
p.setPen(st::defaultMessageBar.titleFg);
_text.drawElided(
p,
st::moreChatsBarTextPosition.x(),
st::moreChatsBarTextPosition.y(),
available);
p.setPen(st::defaultMessageBar.textFg);
_status.drawElided(
p,
st::moreChatsBarStatusPosition.x(),
st::moreChatsBarStatusPosition.y(),
available);
}
void MoreChatsBar::updateControlsGeometry(QRect wrapGeometry) {
const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
if (_shadow->isHidden() != hidden) {
_shadow->setVisible(!hidden);
}
const auto width = std::max(
wrapGeometry.width(),
st::columnMinimalWidthLeft);
_close->move(width - _close->width(), 0);
}
void MoreChatsBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {
_shadowGeometryPostprocess = std::move(postprocess);
updateShadowGeometry(_wrap.geometry());
}
void MoreChatsBar::updateShadowGeometry(QRect wrapGeometry) {
const auto regular = QRect(
wrapGeometry.x(),
wrapGeometry.y() + wrapGeometry.height(),
wrapGeometry.width(),
st::lineWidth);
_shadow->setGeometry(_shadowGeometryPostprocess
? _shadowGeometryPostprocess(regular)
: regular);
}
void MoreChatsBar::show() {
if (!_forceHidden) {
return;
}
_forceHidden = false;
if (_shouldBeShown) {
_wrap.show(anim::type::instant);
_shadow->show();
}
}
void MoreChatsBar::hide() {
if (_forceHidden) {
return;
}
_forceHidden = true;
_wrap.hide(anim::type::instant);
_shadow->hide();
}
void MoreChatsBar::raise() {
_wrap.raise();
_shadow->raise();
}
void MoreChatsBar::finishAnimating() {
_wrap.finishAnimating();
}
void MoreChatsBar::move(int x, int y) {
_wrap.move(x, y);
}
void MoreChatsBar::resizeToWidth(int width) {
_wrap.entity()->resizeToWidth(width);
_inner->resizeToWidth(width);
}
int MoreChatsBar::height() const {
return !_forceHidden
? _wrap.height()
: _shouldBeShown
? st::moreChatsBarHeight
: 0;
}
rpl::producer<int> MoreChatsBar::heightValue() const {
return _wrap.heightValue();
}
rpl::producer<> MoreChatsBar::barClicks() const {
return _barClicks.events();
}
rpl::producer<> MoreChatsBar::closeClicks() const {
return _close->clicks() | rpl::to_empty;
}
} // namespace Ui

View File

@@ -0,0 +1,77 @@
/*
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 "ui/wrap/slide_wrap.h"
#include "ui/effects/animations.h"
#include "ui/text/text.h"
#include "base/object_ptr.h"
#include "base/timer.h"
class Painter;
namespace Ui {
class PlainShadow;
class IconButton;
struct MoreChatsBarContent {
int count = 0;
};
class MoreChatsBar final {
public:
MoreChatsBar(
not_null<QWidget*> parent,
rpl::producer<MoreChatsBarContent> content);
~MoreChatsBar();
[[nodiscard]] not_null<RpWidget*> wrap() {
return &_wrap;
}
void show();
void hide();
void raise();
void finishAnimating();
void setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);
void move(int x, int y);
void resizeToWidth(int width);
[[nodiscard]] int height() const;
[[nodiscard]] rpl::producer<int> heightValue() const;
[[nodiscard]] rpl::producer<> barClicks() const;
[[nodiscard]] rpl::producer<> closeClicks() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _wrap.lifetime();
}
private:
void updateShadowGeometry(QRect wrapGeometry);
void updateControlsGeometry(QRect wrapGeometry);
void setupInner();
void paint(Painter &p);
SlideWrap<> _wrap;
not_null<RpWidget*> _inner;
std::unique_ptr<PlainShadow> _shadow;
object_ptr<IconButton> _close;
rpl::event_stream<> _barClicks;
Fn<QRect(QRect)> _shadowGeometryPostprocess;
bool _shouldBeShown = false;
bool _forceHidden = false;
MoreChatsBarContent _content;
Ui::Text::String _text;
Ui::Text::String _status;
};
} // namespace Ui

Some files were not shown because too many files have changed in this diff Show More