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
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

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,87 @@
/*
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 "info/userpic/info_userpic_bubble_wrap.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_chat.h"
#include "styles/style_info_userpic_builder.h"
namespace Ui {
namespace {
void PaintExcludeTopShadow(QPainter &p, int radius, const QRect &r) {
constexpr auto kHorizontalOffset = 1.;
constexpr auto kVerticalOffset = 2.;
constexpr auto kOpacityStep1 = 0.2;
constexpr auto kOpacityStep2 = 0.4;
const auto opacity = p.opacity();
const auto hOffset = style::ConvertScale(kHorizontalOffset);
const auto vOffset = style::ConvertScale(kVerticalOffset);
p.setOpacity(opacity * kOpacityStep1);
p.drawRoundedRect(
r + QMarginsF(hOffset, -radius, hOffset, 0),
radius,
radius);
p.setOpacity(opacity * kOpacityStep1);
p.drawRoundedRect(
r + QMarginsF(0, 0, 0, vOffset),
radius,
radius);
p.setOpacity(opacity * kOpacityStep2);
p.drawRoundedRect(
r + QMarginsF(0, 0, 0, vOffset / 2.),
radius,
radius);
p.setOpacity(opacity);
}
} // namespace
QRect BubbleWrapInnerRect(const QRect &r) {
return r - st::userpicBuilderEmojiBubblePadding;
}
not_null<Ui::RpWidget*> AddBubbleWrap(
not_null<Ui::VerticalLayout*> container,
const QSize &size) {
const auto bubble = container->add(
object_ptr<Ui::RpWidget>(container),
style::al_top);
bubble->resize(size);
bubble->setNaturalWidth(size.width());
auto cached = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
cached.setDevicePixelRatio(style::DevicePixelRatio());
cached.fill(Qt::transparent);
{
auto p = QPainter(&cached);
const auto innerRect = BubbleWrapInnerRect(bubble->rect());
auto hq = PainterHighQualityEnabler(p);
const auto radius = st::bubbleRadiusSmall;
p.setPen(Qt::NoPen);
p.setBrush(st::shadowFg);
PaintExcludeTopShadow(p, radius, innerRect);
p.setBrush(st::boxBg);
p.drawRoundedRect(innerRect, radius, radius);
}
bubble->paintRequest(
) | rpl::on_next([bubble, cached = std::move(cached)] {
auto p = QPainter(bubble);
p.drawImage(0, 0, cached);
}, bubble->lifetime());
return bubble;
}
} // 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
#include "ui/rp_widget.h"
namespace Ui {
class VerticalLayout;
[[nodiscard]] QRect BubbleWrapInnerRect(const QRect &r);
not_null<Ui::RpWidget*> AddBubbleWrap(
not_null<Ui::VerticalLayout*> container,
const QSize &size);
} // 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
*/
using "ui/basic.style";
using "boxes/boxes.style";
using "ui/widgets/widgets.style";
userpicBuilderEmojiPreviewPadding: margins(0px, 36px, 0px, 8px);
userpicBuilderEmojiSubtitle: FlatLabel(defaultFlatLabel) {
align: align(center);
minWidth: 356px;
maxHeight: 24px;
textFg: windowSubTextFg;
}
userpicBuilderEmojiSubtitlePadding: margins(0px, 9px, 0px, 2px);
userpicBuilderEmojiBubblePaletteWidth: 356px;
userpicBuilderEmojiBubblePalettePadding: margins(12px, 8px, 12px, 8px);
userpicBuilderEmojiSelectorLeft: 5px;
userpicBuilderEmojiButton: RoundButton(defaultBoxButton) {
textFg: boxTextFg;
textFgOver: boxTextFg;
textBg: boxDividerBg;
textBgOver: boxDividerBg;
ripple: universalRippleAnimation;
}
userpicBuilderEmojiBackButton: IconButton(backButton) {
icon: icon {{ "box_button_back", boxTextFg }};
iconOver: icon {{ "box_button_back", boxTextFg }};
ripple: universalRippleAnimation;
}
userpicBuilderEmojiBackPosiiton: point(8px, 8px);
userpicBuilderEmojiSavePosiiton: point(7px, 12px);
userpicBuilderEmojiAccentColorSize: 30px;
userpicBuilderEmojiBubblePadding: margins(5px, 5px, 5px, 5px);
userpicBuilderEmojiLayerMinHeight: 496px;
userpicBuilderEmojiSelectorMinHeight: 216px;
userpicBuilderEmojiSelectorTogglePosition: point(6px, 6px);
userpicBuilderEmojiColorMinus: IconButton(defaultIconButton) {
width: userpicBuilderEmojiAccentColorSize;
height: userpicBuilderEmojiAccentColorSize;
icon: icon {{ "info/info_colors_remove", menuIconFg }};
iconOver: icon {{ "info/info_colors_remove", menuIconFg }};
rippleAreaSize: userpicBuilderEmojiAccentColorSize;
rippleAreaPosition: point(0px, 0px);
ripple: universalRippleAnimation;
}
userpicBuilderEmojiColorPlus: IconButton(userpicBuilderEmojiColorMinus) {
icon: icon {{ "info/info_colors_add", menuIconFg }};
iconOver: icon {{ "info/info_colors_add", menuIconFg }};
}
userpicBuilderEmojiToggleStickersIcon: icon {{ "menu/stickers", emojiIconFg }};

View File

@@ -0,0 +1,42 @@
/*
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 "info/userpic/info_userpic_color_circle_button.h"
#include "settings/settings_chat.h" // Settings::PaintRoundColorButton.
#include "ui/painter.h"
namespace UserpicBuilder {
void CircleButton::setIndex(int index) {
_index = index;
}
int CircleButton::index() const {
return _index;
}
void CircleButton::setBrush(QBrush brush) {
_brush = brush;
update();
}
void CircleButton::setSelectedProgress(float64 progress) {
if (_selectedProgress != progress) {
_selectedProgress = progress;
update();
}
}
void CircleButton::paintEvent(QPaintEvent *event) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto h = height();
Settings::PaintRoundColorButton(p, h, _brush, _selectedProgress);
}
} // namespace UserpicBuilder

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/abstract_button.h"
namespace UserpicBuilder {
class CircleButton final : public Ui::AbstractButton {
public:
using Ui::AbstractButton::AbstractButton;
void setIndex(int index);
[[nodiscard]] int index() const;
void setBrush(QBrush brush);
void setSelectedProgress(float64 progress);
private:
void paintEvent(QPaintEvent *event) override;
int _index = 0;
float64 _selectedProgress = 0.;
QBrush _brush;
};
} // namespace UserpicBuilder

View File

@@ -0,0 +1,316 @@
/*
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 "info/userpic/info_userpic_colors_editor.h"
#include "base/random.h"
#include "ui/wrap/fade_wrap.h"
#include "info/userpic/info_userpic_emoji_builder_preview.h"
#include "info/userpic/info_userpic_color_circle_button.h"
#include "info/userpic/info_userpic_emoji_builder_common.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/color_editor.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/widgets/buttons.h"
#include "ui/vertical_list.h"
#include "ui/rect.h"
#include "styles/style_info_userpic_builder.h"
#include "styles/style_boxes.h"
namespace UserpicBuilder {
namespace {
constexpr auto kMaxColors = int(4);
[[nodiscard]] QColor RandomColor(const QColor &c) {
auto random = bytes::vector(2);
base::RandomFill(random.data(), random.size());
auto result = QColor();
result.setHslF(
(uchar(random[0]) % 100) / 100.,
(uchar(random[1]) % 50) / 100. + 0.5,
c.lightnessF());
return result;
}
class ColorsLine final : public Ui::RpWidget {
public:
using Chosen = CircleButton;
using Success = bool;
ColorsLine(
not_null<Ui::RpWidget*> parent,
not_null<std::vector<QColor>*> colors);
void init();
void fillButtons();
[[nodiscard]] Chosen *chosen() const;
[[nodiscard]] rpl::producer<Chosen*> chosenChanges() const;
private:
struct ButtonState {
bool shown = false;
int left = 0;
};
[[nodiscard]] std::vector<ButtonState> calculatePositionFor(int count);
void processChange(
const std::vector<QColor> wasColors,
const std::vector<QColor> nowColors);
void setLastChosen() const;
const not_null<std::vector<QColor>*> _colors;
base::unique_qptr<Ui::RpWidget> _container;
std::vector<not_null<CircleButton*>> _colorButtons;
std::vector<not_null<Ui::FadeWrap<Ui::RpWidget>*>> _wraps;
Ui::Animations::Simple _chooseAnimation;
Ui::Animations::Simple _positionAnimation;
Chosen *_chosen = nullptr;
rpl::event_stream<Chosen*> _chosenChanges;
};
ColorsLine::ColorsLine(
not_null<Ui::RpWidget*> parent,
not_null<std::vector<QColor>*> colors)
: Ui::RpWidget(parent)
, _colors(colors) {
}
void ColorsLine::init() {
fillButtons();
processChange(*_colors, *_colors);
setLastChosen();
}
void ColorsLine::fillButtons() {
_container = base::make_unique_q<Ui::RpWidget>(this);
const auto container = _container.get();
sizeValue(
) | rpl::on_next([=](const QSize &s) {
container->setGeometry(Rect(s));
}, container->lifetime());
const auto minus = Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
container,
object_ptr<Ui::IconButton>(
container,
st::userpicBuilderEmojiColorMinus));
_wraps.push_back(minus);
minus->toggle(_colors->size() > 1, anim::type::instant);
minus->entity()->setClickedCallback([=] {
if (_colors->size() < 2) {
return;
}
const auto wasColors = *_colors;
_colors->erase(_colors->end() - 1);
const auto nowColors = *_colors;
processChange(wasColors, nowColors);
setLastChosen();
});
for (auto i = 0; i < kMaxColors; i++) {
const auto wrap = Ui::CreateChild<Ui::FadeWrap<CircleButton>>(
container,
object_ptr<CircleButton>(container));
const auto button = wrap->entity();
button->resize(height(), height());
button->setIndex(i);
_wraps.push_back(wrap);
_colorButtons.push_back(button);
button->setClickedCallback([=] {
const auto wasChosen = _chosen;
_chosen = button;
const auto nowChosen = _chosen;
_chosenChanges.fire_copy(_chosen);
_chooseAnimation.stop();
_chooseAnimation.start([=](float64 progress) {
if (wasChosen) {
wasChosen->setSelectedProgress(1. - progress);
}
nowChosen->setSelectedProgress(progress);
}, 0., 1., st::universalDuration);
});
if (i < _colors->size()) {
button->setBrush((*_colors)[i]);
} else {
wrap->hide(anim::type::instant);
}
}
const auto plus = Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
container,
object_ptr<Ui::IconButton>(
container,
st::userpicBuilderEmojiColorPlus));
_wraps.push_back(plus);
plus->toggle(_colors->size() < kMaxColors, anim::type::instant);
plus->entity()->setClickedCallback([=] {
if (_colors->size() >= kMaxColors) {
return;
}
const auto wasColors = *_colors;
_colors->push_back(RandomColor(_colors->back()));
const auto nowColors = *_colors;
processChange(wasColors, nowColors);
setLastChosen();
});
for (const auto &wrap : _wraps) {
wrap->setDuration(st::universalDuration);
}
}
std::vector<ColorsLine::ButtonState> ColorsLine::calculatePositionFor(
int count) {
// Minus - Color - Color - Color - Color - Plus.
auto result = std::vector<ButtonState>(6);
const auto fullWidth = _container->width();
const auto width = _container->height();
const auto colorsWidth = width * count + width * (count - 1);
const auto left = (fullWidth - colorsWidth) / 2;
for (auto i = 0; i < _colorButtons.size(); i++) {
result[i + 1] = {
.shown = (i < count),
.left = left + (i * width * 2),
};
}
result[0] = {
.shown = (count > 1),
.left = (left - width * 2),
};
result[result.size() - 1] = {
.shown = (count < kMaxColors),
.left = (left + colorsWidth + width),
};
return result;
}
void ColorsLine::processChange(
const std::vector<QColor> wasColors,
const std::vector<QColor> nowColors) {
const auto wasPosition = calculatePositionFor(wasColors.size());
const auto nowPosition = calculatePositionFor(nowColors.size());
for (auto i = 0; i < nowPosition.size(); i++) {
const auto colorIndex = i - 1;
if ((colorIndex > 0) && (colorIndex < _colors->size())) {
_colorButtons[colorIndex]->setBrush((*_colors)[colorIndex]);
}
_wraps[i]->toggle(nowPosition[i].shown, anim::type::normal);
}
_positionAnimation.stop();
_positionAnimation.start([=](float64 value) {
for (auto i = 0; i < nowPosition.size(); i++) {
const auto wasLeft = wasPosition[i].left;
const auto nowLeft = nowPosition[i].left;
const auto left = anim::interpolate(wasLeft, nowLeft, value);
_wraps[i]->moveToLeft(left, 0);
}
}, 0., 1., st::universalDuration);
}
void ColorsLine::setLastChosen() const {
for (auto i = 0; i < _colorButtons.size(); i++) {
if (i == (_colors->size() - 1)) {
_colorButtons[i]->clicked({}, Qt::LeftButton);
}
}
}
ColorsLine::Chosen *ColorsLine::chosen() const {
return _chosen;
}
rpl::producer<ColorsLine::Chosen*> ColorsLine::chosenChanges() const {
return _chosenChanges.events();
}
} // namespace
object_ptr<Ui::RpWidget> CreateGradientEditor(
not_null<Ui::RpWidget*> parent,
DocumentData *document,
std::vector<QColor> startColors,
BothWayCommunication<std::vector<QColor>> communication) {
auto container = object_ptr<Ui::VerticalLayout>(parent.get());
struct State {
std::vector<QColor> colors;
};
const auto preview = container->add(
object_ptr<EmojiUserpic>(
container,
Size(st::defaultUserpicButton.photoSize),
false),
style::al_top);
preview->setDuration(0);
if (document) {
preview->setDocument(document);
}
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
const auto state = container->lifetime().make_state<State>();
state->colors = std::move(startColors);
const auto buttonsContainer = container->add(object_ptr<ColorsLine>(
container,
&state->colors));
buttonsContainer->resize(0, st::userpicBuilderEmojiAccentColorSize);
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
auto ownedEditor = object_ptr<ColorEditor>(
container,
ColorEditor::Mode::HSL,
state->colors.back());
container->resizeToWidth(ownedEditor->width());
const auto editor = container->add(std::move(ownedEditor));
buttonsContainer->chosenChanges(
) | rpl::on_next([=](ColorsLine::Chosen *chosen) {
if (chosen) {
const auto color = state->colors[chosen->index()];
editor->showColor(color);
editor->setCurrent(color);
}
}, editor->lifetime());
const auto save = crl::guard(container.data(), [=] {
communication.result(state->colors);
});
// editor->submitRequests(
// ) | rpl::on_next([=] {
// }, editor->lifetime());
editor->colorValue(
) | rpl::on_next([=](QColor c) {
if (const auto chosen = buttonsContainer->chosen()) {
chosen->setBrush(c);
state->colors[chosen->index()] = c;
}
preview->setGradientColors(state->colors);
}, preview->lifetime());
base::take(
communication.triggers
) | rpl::on_next([=] {
save();
}, container->lifetime());
buttonsContainer->init();
return container;
}
} // namespace UserpicBuilder

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
template <typename Object>
class object_ptr;
class DocumentData;
namespace Ui {
class RpWidget;
} // namespace Ui
namespace UserpicBuilder {
template <typename Result>
struct BothWayCommunication;
[[nodiscard]] object_ptr<Ui::RpWidget> CreateGradientEditor(
not_null<Ui::RpWidget*> parent,
DocumentData *document,
std::vector<QColor> startColors,
BothWayCommunication<std::vector<QColor>> communication);
} // namespace UserpicBuilder

View File

@@ -0,0 +1,78 @@
/*
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 "info/userpic/info_userpic_emoji_builder.h"
#include "info/userpic/info_userpic_emoji_builder_common.h"
#include "info/userpic/info_userpic_emoji_builder_layer.h"
#include "info/userpic/info_userpic_emoji_builder_widget.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "styles/style_info_userpic_builder.h"
namespace UserpicBuilder {
void ShowLayer(
not_null<Window::SessionController*> controller,
StartData data,
Fn<void(UserpicBuilder::Result)> &&doneCallback) {
auto layer = std::make_unique<LayerWidget>();
const auto layerRaw = layer.get();
{
struct State {
rpl::event_stream<> clicks;
};
const auto state = layer->lifetime().make_state<State>();
const auto content = CreateUserpicBuilder(
layerRaw,
controller,
data,
BothWayCommunication<UserpicBuilder::Result>{
.triggers = state->clicks.events(),
.result = [=, done = std::move(doneCallback)](Result r) {
done(std::move(r));
layerRaw->closeLayer();
},
});
const auto save = Ui::CreateChild<Ui::RoundButton>(
content.get(),
tr::lng_connection_save(),
st::userpicBuilderEmojiButton);
save->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
content->sizeValue(
) | rpl::on_next([=] {
const auto &p = st::userpicBuilderEmojiSavePosiiton;
save->moveToRight(p.x(), p.y());
}, save->lifetime());
save->clicks() | rpl::to_empty | rpl::start_to_stream(
state->clicks,
save->lifetime());
const auto back = Ui::CreateChild<Ui::IconButton>(
content.get(),
st::userpicBuilderEmojiBackButton);
back->setClickedCallback([=] {
layerRaw->closeLayer();
});
content->sizeValue(
) | rpl::on_next([=] {
const auto &p = st::userpicBuilderEmojiBackPosiiton;
back->moveToLeft(p.x(), p.y());
}, back->lifetime());
layer->setContent(content);
}
controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
} // namespace UserpicBuilder

View File

@@ -0,0 +1,24 @@
/*
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 Window {
class SessionController;
} // namespace Window
namespace UserpicBuilder {
struct Result;
struct StartData;
void ShowLayer(
not_null<Window::SessionController*> controller,
StartData data,
Fn<void(UserpicBuilder::Result)> &&doneCallback);
} // namespace UserpicBuilder

View File

@@ -0,0 +1,53 @@
/*
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 "info/userpic/info_userpic_emoji_builder_common.h"
#include "ui/image/image_prepare.h"
#include "ui/userpic_view.h" // ForumUserpicRadiusMultiplier.
namespace UserpicBuilder {
[[nodiscard]] QImage GenerateGradient(
const QSize &size,
const std::vector<QColor> &colors,
bool circle,
bool roundForumRect) {
constexpr auto kRotation = int(45);
auto gradient = Images::GenerateGradient(size, colors, kRotation);
if (!circle && !roundForumRect) {
return gradient;
}
const auto processModifier = [&](QImage &&i) {
if (circle) {
return Images::Circle(std::move(i));
} else if (roundForumRect) {
const auto radius = std::min(i.height(), i.width())
* Ui::ForumUserpicRadiusMultiplier();
return Images::Round(
std::move(i),
Images::CornersMask(radius / style::DevicePixelRatio()));
} else {
return std::move(i);
}
};
if (style::DevicePixelRatio() == 1) {
return processModifier(std::move(gradient));
}
auto image = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
p.drawImage(QRect(QPoint(), size), gradient);
}
return processModifier(std::move(image));
}
} // namespace UserpicBuilder

View File

@@ -0,0 +1,38 @@
/*
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 UserpicBuilder {
struct Result {
QImage&& image;
DocumentId id = 0;
std::vector<QColor> colors;
};
[[nodiscard]] QImage GenerateGradient(
const QSize &size,
const std::vector<QColor> &colors,
bool circle = true,
bool roundForumRect = false);
struct StartData {
DocumentId documentId = DocumentId(0);
int builderColorIndex = 0;
rpl::producer<std::vector<DocumentId>> documents;
std::vector<QColor> gradientEditorColors;
bool isForum = false;
};
template <typename Result>
struct BothWayCommunication {
rpl::producer<> triggers;
Fn<void(Result)> result;
};
} // namespace UserpicBuilder

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
*/
#include "info/userpic/info_userpic_emoji_builder_layer.h"
#include "styles/style_info.h"
#include "styles/style_info_userpic_builder.h"
#include "styles/style_layers.h"
namespace UserpicBuilder {
LayerWidget::LayerWidget()
: _corners(Ui::PrepareCornerPixmaps(st::boxRadius, st::boxDividerBg)) {
}
void LayerWidget::setContent(not_null<Ui::RpWidget*> content) {
_content = content;
}
void LayerWidget::parentResized() {
Expects(_content != nullptr);
const auto parentSize = parentWidget()->size();
const auto currentHeight = resizeGetHeight(0);
const auto currentWidth = _content->width();
resizeToWidth(currentWidth);
moveToLeft(
(parentSize.width() - currentWidth) / 2,
(parentSize.height() - currentHeight) / 2);
}
bool LayerWidget::closeByOutsideClick() const {
return false;
}
int LayerWidget::resizeGetHeight(int newWidth) {
Expects(_content != nullptr);
_content->resizeToWidth(st::infoDesiredWidth);
return st::userpicBuilderEmojiLayerMinHeight;
}
void LayerWidget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
Ui::FillRoundRect(p, rect(), st::boxDividerBg, _corners);
}
} // namespace UserpicBuilder

View File

@@ -0,0 +1,39 @@
/*
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/layer_widget.h"
#include "ui/cached_round_corners.h"
namespace Ui {
class RpWidget;
} // namespace Ui
namespace UserpicBuilder {
class LayerWidget : public Ui::LayerWidget {
public:
LayerWidget();
void setContent(not_null<Ui::RpWidget*> content);
void parentResized() override;
bool closeByOutsideClick() const override;
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
const Ui::CornersPixmaps _corners;
Ui::RpWidget *_content;
};
} // namespace UserpicBuilder

View File

@@ -0,0 +1,213 @@
/*
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 "info/userpic/info_userpic_emoji_builder_menu_item.h"
#include "base/random.h"
#include "base/timer.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/userpic/info_userpic_emoji_builder.h"
#include "info/userpic/info_userpic_emoji_builder_common.h"
#include "info/userpic/info_userpic_emoji_builder_widget.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/menu/menu_common.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_menu_icons.h"
#include <random>
namespace UserpicBuilder {
namespace {
constexpr auto kTimeout = crl::time(1500);
class StickerProvider final {
public:
StickerProvider(not_null<Data::Session*> owner);
void setDocuments(std::vector<DocumentId> documents);
[[nodiscard]] DocumentId documentId() const;
[[nodiscard]] auto documentChanged() const
-> rpl::producer<not_null<DocumentData*>>;
private:
void processDocumentIndex(int documentIndex);
[[nodiscard]] DocumentData *lookupAndRememberSticker(int documentIndex);
[[nodiscard]] std::pair<DocumentData*, int> lookupSticker(
int documentIndex) const;
const not_null<Data::Session*> _owner;
int _documentIndex = 0;
std::vector<DocumentId> _shuffledDocuments;
base::Timer _timer;
rpl::event_stream<not_null<DocumentData*>> _documentChanged;
rpl::lifetime _resolvingLifetime;
rpl::lifetime _downloadFinishedLifetime;
};
StickerProvider::StickerProvider(not_null<Data::Session*> owner)
: _owner(owner) {
_timer.setCallback([=] {
_documentIndex++;
if (_documentIndex >= _shuffledDocuments.size()) {
_documentIndex = 0;
}
processDocumentIndex(_documentIndex);
});
}
DocumentId StickerProvider::documentId() const {
const auto &[document, index] = lookupSticker(_documentIndex);
return document ? document->id : DocumentId(0);
}
void StickerProvider::setDocuments(std::vector<DocumentId> documents) {
if (documents.empty()) {
return;
}
auto rd = std::random_device();
ranges::shuffle(documents, std::mt19937(rd()));
_shuffledDocuments = std::move(documents);
_documentIndex = 0;
processDocumentIndex(_documentIndex);
}
auto StickerProvider::documentChanged() const
-> rpl::producer<not_null<DocumentData*>> {
return _documentChanged.events();
}
void StickerProvider::processDocumentIndex(int documentIndex) {
if (const auto document = lookupAndRememberSticker(documentIndex)) {
_resolvingLifetime.destroy();
_owner->customEmojiManager().resolve(
document->id
) | rpl::on_next([=](not_null<DocumentData*> d) {
_resolvingLifetime.destroy();
_downloadFinishedLifetime.destroy();
const auto mediaView = d->createMediaView();
_downloadFinishedLifetime.add([=] {
[[maybe_unused]] const auto copy = mediaView;
});
mediaView->checkStickerLarge();
mediaView->goodThumbnailWanted();
rpl::single(
rpl::empty_value()
) | rpl::then(
_owner->session().downloaderTaskFinished()
) | rpl::on_next([=] {
if (mediaView->loaded()) {
_timer.callOnce(kTimeout);
_documentChanged.fire_copy(mediaView->owner());
_downloadFinishedLifetime.destroy();
}
}, _downloadFinishedLifetime);
}, _resolvingLifetime);
} else if (!_resolvingLifetime) {
_timer.callOnce(kTimeout);
}
}
DocumentData *StickerProvider::lookupAndRememberSticker(int documentIndex) {
const auto &[document, index] = lookupSticker(documentIndex);
if (document) {
_documentIndex = index;
}
return document;
}
std::pair<DocumentData*, int> StickerProvider::lookupSticker(
int documentIndex) const {
const auto size = _shuffledDocuments.size();
for (auto i = 0; i < size; i++) {
const auto unrestrictedIndex = documentIndex + i;
const auto index = (unrestrictedIndex >= size)
? (unrestrictedIndex - size)
: unrestrictedIndex;
const auto id = _shuffledDocuments[index];
const auto document = _owner->document(id);
if (document->sticker()) {
return { document, index };
}
}
return { nullptr, 0 };
}
} // namespace
void AddEmojiBuilderAction(
not_null<Window::SessionController*> controller,
not_null<Ui::PopupMenu*> menu,
rpl::producer<std::vector<DocumentId>> documents,
Fn<void(UserpicBuilder::Result)> &&done,
bool isForum) {
struct State final {
State(not_null<Window::SessionController*> controller)
: manager(&controller->session().data())
, colorIndex(rpl::single(
rpl::empty_value()
) | rpl::then(
manager.documentChanged() | rpl::skip(1) | rpl::to_empty
) | rpl::map([] {
return base::RandomIndex(std::numeric_limits<int>::max());
})) {
}
StickerProvider manager;
rpl::variable<int> colorIndex;
};
const auto state = menu->lifetime().make_state<State>(controller);
auto item = base::make_unique_q<Ui::Menu::Action>(
menu.get(),
menu->st().menu,
Ui::Menu::CreateAction(
menu.get(),
tr::lng_attach_profile_emoji(tr::now),
[=, done = std::move(done), docs = rpl::duplicate(documents)] {
const auto id = state->manager.documentId();
UserpicBuilder::ShowLayer(
controller,
{ id, state->colorIndex.current(), docs, {}, isForum },
base::duplicate(done));
}),
nullptr,
nullptr);
const auto icon = UserpicBuilder::CreateEmojiUserpic(
item.get(),
st::restoreUserpicIcon.size,
state->manager.documentChanged(),
state->colorIndex.value(),
isForum);
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
icon->move(menu->st().menu.itemIconPosition
+ QPoint(
(st::menuIconRemove.width() - icon->width()) / 2,
(st::menuIconRemove.height() - icon->height()) / 2));
rpl::duplicate(
documents
) | rpl::on_next([=](std::vector<DocumentId> documents) {
state->manager.setDocuments(std::move(documents));
}, item->lifetime());
menu->addAction(std::move(item));
}
} // namespace UserpicBuilder

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
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace UserpicBuilder {
struct Result;
void AddEmojiBuilderAction(
not_null<Window::SessionController*> controller,
not_null<Ui::PopupMenu*> menu,
rpl::producer<std::vector<DocumentId>> documents,
Fn<void(UserpicBuilder::Result)> &&done,
bool isForum);
} // namespace UserpicBuilder

View File

@@ -0,0 +1,250 @@
/*
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 "info/userpic/info_userpic_emoji_builder_preview.h"
#include "chat_helpers/stickers_lottie.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
#include "history/view/media/history_view_sticker_player.h"
#include "info/userpic/info_userpic_emoji_builder_common.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/painter.h"
#include "ui/rect.h"
namespace UserpicBuilder {
PreviewPainter::PreviewPainter(int size)
: _size(size)
, _emojiSize(base::SafeRound(_size / M_SQRT2))
, _frameGeometry(Rect(Size(_size)) - Margins((_size - _emojiSize) / 2))
, _frameRect(Rect(_frameGeometry.size()))
, _mask(
_frameRect.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied)
, _frame(_mask.size(), QImage::Format_ARGB32_Premultiplied) {
_frame.setDevicePixelRatio(style::DevicePixelRatio());
_mask.setDevicePixelRatio(style::DevicePixelRatio());
_mask.fill(Qt::transparent);
{
auto p = QPainter(&_mask);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBg);
constexpr auto kFrameRadiusPercent = 25;
p.drawRoundedRect(
_frameRect,
kFrameRadiusPercent,
kFrameRadiusPercent,
Qt::RelativeSize);
}
}
DocumentData *PreviewPainter::document() const {
return _media ? _media->owner().get() : nullptr;
}
void PreviewPainter::setPlayOnce(bool value) {
_playOnce = value;
}
void PreviewPainter::setDocument(
not_null<DocumentData*> document,
Fn<void()> updateCallback) {
if (_media && (document == _media->owner())) {
return;
}
_lifetime.destroy();
const auto sticker = document->sticker();
Assert(sticker != nullptr);
_media = document->createMediaView();
_media->checkStickerLarge();
_media->goodThumbnailWanted();
if (_playOnce) {
_firstFrameShown = false;
_paused = false;
} else {
_paused = true;
}
rpl::single() | rpl::then(
document->session().downloaderTaskFinished()
) | rpl::on_next([=] {
if (!_media->loaded()) {
return;
}
_lifetime.destroy();
const auto emojiSize = Size(_size * style::DevicePixelRatio());
if (sticker->isLottie()) {
_player = std::make_unique<HistoryView::LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
_media.get(),
//
ChatHelpers::StickerLottieSize::EmojiInteractionReserved7,
emojiSize,
Lottie::Quality::High));
} else if (sticker->isWebm()) {
_player = std::make_unique<HistoryView::WebmPlayer>(
_media->owner()->location(),
_media->bytes(),
emojiSize);
} else if (sticker) {
_player = std::make_unique<HistoryView::StaticStickerPlayer>(
_media->owner()->location(),
_media->bytes(),
emojiSize);
}
if (_player) {
_player->setRepaintCallback(updateCallback);
} else if (updateCallback) {
updateCallback();
}
}, _lifetime);
}
void PreviewPainter::paintBackground(QPainter &p, const QImage &image) {
p.drawImage(0, 0, image);
}
bool PreviewPainter::paintForeground(QPainter &p) {
if (_player && _player->ready()) {
const auto c = _media->owner()->emojiUsesTextColor() ? 255 : 0;
auto frame = _player->frame(
Size(_emojiSize),
QColor(c, c, c, c),
false,
crl::now(),
_paused);
if (_playOnce) {
if (!_firstFrameShown && (frame.index == 1)) {
_firstFrameShown = true;
} else if (_firstFrameShown && !frame.index) {
_paused = true;
}
}
_frame.fill(Qt::transparent);
{
QPainter q(&_frame);
if (frame.image.width() == frame.image.height()) {
q.drawImage(_frameRect, frame.image);
} else {
auto frameRect = Rect(frame.image.size().scaled(
_frameRect.size(),
Qt::KeepAspectRatio));
frameRect.moveCenter(_frameRect.center());
q.drawImage(frameRect, frame.image);
}
q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
q.drawImage(0, 0, _mask);
}
p.drawImage(_frameGeometry.topLeft(), _frame);
if (!_paused) {
_player->markFrameShown();
}
return true;
}
return false;
}
EmojiUserpic::EmojiUserpic(
not_null<Ui::RpWidget*> parent,
const QSize &size,
bool isForum)
: Ui::RpWidget(parent)
, _forum(isForum)
, _painter(size.width())
, _duration(st::slideWrapDuration) {
resize(size);
setNaturalWidth(size.width());
}
void EmojiUserpic::setDocument(not_null<DocumentData*> document) {
if (!_playOnce.has_value()) {
const auto &c = document->session().appConfig();
_playOnce = !c.get<bool>(u"upload_markup_video"_q, false);
}
_painter.setDocument(document, [=] { update(); });
_painter.setPlayOnce(*_playOnce);
}
void EmojiUserpic::result(int size, Fn<void(UserpicBuilder::Result)> done) {
const auto painter = lifetime().make_state<PreviewPainter>(size);
// Reset to the first frame.
const auto document = _painter.document();
const auto callback = [=] {
auto background = GenerateGradient(Size(size), _colors, false);
{
constexpr auto kAttemptsToDrawFirstFrame = 3000;
auto attempts = 0;
auto p = QPainter(&background);
while (attempts < kAttemptsToDrawFirstFrame) {
if (painter->paintForeground(p)) {
break;
}
attempts++;
}
}
if (*_playOnce && document) {
done({ std::move(background), document->id, _colors });
} else {
done({ std::move(background) });
}
};
if (document) {
painter->setDocument(document, callback);
} else {
callback();
}
}
void EmojiUserpic::setGradientColors(std::vector<QColor> colors) {
if (_colors == colors) {
return;
}
if (const auto colors = base::take(_colors); !colors.empty()) {
_previousImage = GenerateGradient(size(), colors, !_forum, _forum);
}
_colors = std::move(colors);
{
_image = GenerateGradient(size(), _colors, !_forum, _forum);
}
if (_duration) {
_animation.stop();
_animation.start([=] { update(); }, 0., 1., _duration);
} else {
update();
}
}
void EmojiUserpic::paintEvent(QPaintEvent *event) {
auto p = QPainter(this);
if (_animation.animating() && !_previousImage.isNull()) {
_painter.paintBackground(p, _previousImage);
p.setOpacity(_animation.value(1.));
}
_painter.paintBackground(p, _image);
p.setOpacity(1.);
_painter.paintForeground(p);
}
void EmojiUserpic::setDuration(crl::time duration) {
_duration = duration;
}
} // namespace UserpicBuilder

View File

@@ -0,0 +1,90 @@
/*
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/effects/animations.h"
namespace Data {
class DocumentMedia;
} // namespace Data
namespace HistoryView {
class StickerPlayer;
} // namespace HistoryView
class DocumentData;
namespace UserpicBuilder {
struct Result;
class PreviewPainter final {
public:
PreviewPainter(int size);
[[nodiscard]] DocumentData *document() const;
void setPlayOnce(bool value);
void setDocument(
not_null<DocumentData*> document,
Fn<void()> updateCallback);
void paintBackground(QPainter &p, const QImage &image);
bool paintForeground(QPainter &p);
private:
const int _size;
const int _emojiSize;
const QRect _frameGeometry;
const QRect _frameRect;
QImage _mask;
QImage _frame;
std::shared_ptr<Data::DocumentMedia> _media;
std::unique_ptr<HistoryView::StickerPlayer> _player;
bool _playOnce = false;
bool _paused = false;
bool _firstFrameShown = false;
rpl::lifetime _lifetime;
};
class EmojiUserpic final : public Ui::RpWidget {
public:
EmojiUserpic(
not_null<Ui::RpWidget*> parent,
const QSize &size,
bool isForum);
void result(int size, Fn<void(UserpicBuilder::Result)> done);
void setGradientColors(std::vector<QColor> colors);
void setDocument(not_null<DocumentData*> document);
void setDuration(crl::time duration);
protected:
void paintEvent(QPaintEvent *event) override;
private:
const bool _forum;
PreviewPainter _painter;
std::optional<bool> _playOnce;
QImage _previousImage;
QImage _image;
std::vector<QColor> _colors;
crl::time _duration;
Ui::Animations::Simple _animation;
};
} // namespace UserpicBuilder

View File

@@ -0,0 +1,578 @@
/*
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 "info/userpic/info_userpic_emoji_builder_widget.h"
#include "api/api_peer_photo.h"
#include "apiwrap.h"
#include "chat_helpers/emoji_list_widget.h"
#include "chat_helpers/stickers_list_widget.h"
#include "data/data_document.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "editor/photo_editor_layer_widget.h" // Editor::kProfilePhotoSize.
#include "info/userpic/info_userpic_bubble_wrap.h"
#include "info/userpic/info_userpic_color_circle_button.h"
#include "info/userpic/info_userpic_colors_editor.h"
#include "info/userpic/info_userpic_emoji_builder_common.h"
#include "info/userpic/info_userpic_emoji_builder_preview.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/controls/emoji_button.h"
#include "ui/empty_userpic.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info_userpic_builder.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "styles/style_menu_icons.h"
namespace UserpicBuilder {
namespace {
void AlignChildren(not_null<Ui::RpWidget*> widget, int fullWidth) {
const auto children = widget->children();
const auto widgets = ranges::views::all(
children
) | ranges::views::filter([](not_null<const QObject*> object) {
return object->isWidgetType();
}) | ranges::views::transform([](not_null<QObject*> object) {
return static_cast<QWidget*>(object.get());
}) | ranges::to_vector;
const auto widgetWidth = widgets.front()->width();
const auto widgetsCount = widgets.size();
const auto widgetsWidth = widgetWidth * widgetsCount;
const auto step = (fullWidth - widgetsWidth) / (widgetsCount - 1);
for (auto i = 0; i < widgetsCount; i++) {
widgets[i]->move(i * (widgetWidth + step), widgets[i]->y());
}
}
[[nodiscard]] QImage GenerateSpecial(
int size,
const std::vector<QColor> colors) {
if (colors.empty()) {
auto image = QImage(
Size(size * style::DevicePixelRatio()),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
st::userpicBuilderEmojiColorPlus.icon.paintInCenter(
p,
Rect(Size(size)));
}
return image;
} else {
auto image = GenerateGradient(Size(size), colors);
{
auto p = QPainter(&image);
constexpr auto kEllipseSize = 1;
const auto center = QPointF(size / 2., size / 2.);
const auto shift = QPointF(kEllipseSize * 4, 0);
p.setPen(Qt::NoPen);
p.setBrush(st::boxBg);
p.drawEllipse(center, kEllipseSize, kEllipseSize);
p.drawEllipse(center + shift, kEllipseSize, kEllipseSize);
p.drawEllipse(center - shift, kEllipseSize, kEllipseSize);
}
return image;
}
}
[[nodiscard]] std::vector<std::vector<QColor>> PaletteGradients() {
auto v = std::vector<std::vector<QColor>>{
{
QColor(32, 226, 205),
QColor(14, 225, 241),
QColor(77, 141, 255),
QColor(43, 191, 255),
},
{
QColor(69, 247, 183),
QColor(31, 241, 217),
QColor(94, 182, 251),
QColor(31, 206, 235),
},
{
QColor(193, 229, 38),
QColor(128, 223, 43),
QColor(9, 210, 96),
QColor(94, 220, 64),
},
{
QColor(255, 212, 18),
QColor(255, 167, 67),
QColor(245, 105, 78),
QColor(245, 119, 44),
},
{
QColor(246, 167, 48),
QColor(255, 119, 66),
QColor(246, 72, 132),
QColor(239, 91, 65),
},
{
QColor(255, 178, 58),
QColor(254, 126, 98),
QColor(249, 75, 160),
QColor(251, 92, 128),
},
{
QColor(255, 114, 169),
QColor(226, 105, 255),
QColor(131, 124, 255),
QColor(176, 99, 255),
},
};
for (auto &g : v) {
// Rotate 180 degrees.
std::swap(g[0], g[2]);
std::swap(g[1], g[3]);
}
return v;
}
void ShowGradientEditor(
not_null<Window::SessionController*> controller,
StartData data,
Fn<void(std::vector<QColor>)> &&doneCallback) {
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
rpl::event_stream<> saveRequests;
};
const auto state = box->lifetime().make_state<State>();
box->setTitle(tr::lng_chat_theme_change());
box->addButton(tr::lng_settings_save(), [=] {
state->saveRequests.fire({});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
auto content = CreateGradientEditor(
box,
(data.documentId
? controller->session().data().document(
data.documentId).get()
: nullptr),
data.gradientEditorColors,
BothWayCommunication<std::vector<QColor>>{
state->saveRequests.events(),
[=](std::vector<QColor> colors) {
box->closeBox();
doneCallback(std::move(colors));
},
});
box->setWidth(content->width());
box->addRow(std::move(content), style::margins());
}));
}
class EmojiSelector final : public Ui::RpWidget {
public:
EmojiSelector(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
rpl::producer<std::vector<DocumentId>> recent);
[[nodiscard]] rpl::producer<not_null<DocumentData*>> chosen() const;
private:
using Footer = ChatHelpers::TabbedSelector::InnerFooter;
using List = ChatHelpers::TabbedSelector::Inner;
using Type = ChatHelpers::SelectorTab;
void createSelector(Type type);
struct Selector {
not_null<List*> list;
not_null<Footer*> footer;
};
[[nodiscard]] Selector createEmojiList(
not_null<Ui::ScrollArea*> scroll);
[[nodiscard]] Selector createStickersList(
not_null<Ui::ScrollArea*> scroll) const;
const not_null<Window::SessionController*> _controller;
base::unique_qptr<Ui::RpWidget> _container;
rpl::event_stream<> _recentChanges;
std::vector<DocumentId> _lastRecent;
rpl::event_stream<not_null<DocumentData*>> _chosen;
};
EmojiSelector::EmojiSelector(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
rpl::producer<std::vector<DocumentId>> recent)
: RpWidget(parent)
, _controller(controller) {
std::move(
recent
) | rpl::on_next([=](std::vector<DocumentId> ids) {
_lastRecent = std::move(ids);
_recentChanges.fire({});
}, lifetime());
createSelector(Type::Emoji);
}
rpl::producer<not_null<DocumentData*>> EmojiSelector::chosen() const {
return _chosen.events();
}
EmojiSelector::Selector EmojiSelector::createEmojiList(
not_null<Ui::ScrollArea*> scroll) {
const auto session = &_controller->session();
const auto manager = &session->data().customEmojiManager();
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
auto args = ChatHelpers::EmojiListDescriptor{
.show = _controller->uiShow(),
.mode = ChatHelpers::EmojiListMode::UserpicBuilder,
.paused = [=] { return true; },
.customRecentList = ChatHelpers::DocumentListToRecent(_lastRecent),
.customRecentFactory = [=](DocumentId id, Fn<void()> repaint) {
return manager->create(id, std::move(repaint), tag);
},
.st = &st::userpicBuilderEmojiPan,
};
const auto list = scroll->setOwnedWidget(
object_ptr<ChatHelpers::EmojiListWidget>(scroll, std::move(args)));
const auto footer = list->createFooter().data();
list->refreshEmoji();
list->customChosen(
) | rpl::on_next([=](const ChatHelpers::FileChosen &chosen) {
_chosen.fire_copy(chosen.document);
}, list->lifetime());
_recentChanges.events(
) | rpl::on_next([=] {
createSelector(Type::Emoji);
}, list->lifetime());
list->setAllowWithoutPremium(true);
return { list, footer };
}
EmojiSelector::Selector EmojiSelector::createStickersList(
not_null<Ui::ScrollArea*> scroll) const {
const auto list = scroll->setOwnedWidget(
object_ptr<ChatHelpers::StickersListWidget>(
scroll,
_controller,
Window::GifPauseReason::Any,
ChatHelpers::StickersListMode::UserpicBuilder));
const auto footer = list->createFooter().data();
list->refreshRecent();
list->chosen(
) | rpl::on_next([=](const ChatHelpers::FileChosen &chosen) {
_chosen.fire_copy(chosen.document);
}, list->lifetime());
return { list, footer };
}
void EmojiSelector::createSelector(Type type) {
Expects((type == Type::Emoji) || (type == Type::Stickers));
const auto isEmoji = (type == Type::Emoji);
const auto &stScroll = st::reactPanelScroll;
_container = base::make_unique_q<Ui::RpWidget>(this);
const auto container = _container.get();
container->show();
sizeValue(
) | rpl::on_next([=](const QSize &s) {
container->setGeometry(Rect(s));
}, container->lifetime());
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(container, stScroll);
const auto selector = isEmoji
? createEmojiList(scroll)
: createStickersList(scroll);
selector.footer->setParent(container);
const auto toggleButton = Ui::CreateChild<Ui::AbstractButton>(container);
const auto &togglePos = st::userpicBuilderEmojiSelectorTogglePosition;
{
const auto &pos = togglePos;
toggleButton->resize(st::menuIconStickers.size()
// Trying to overlap the settings button under.
+ QSize(pos.x() * 2, pos.y() * 2));
toggleButton->show();
toggleButton->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(toggleButton);
const auto r = toggleButton->rect()
- QMargins(pos.x(), pos.y(), pos.x(), pos.y());
p.fillRect(r, st::boxBg);
if (isEmoji) {
st::userpicBuilderEmojiToggleStickersIcon.paintInCenter(p, r);
} else {
st::defaultEmojiPan.icons.people.paintInCenter(p, r);
}
}, toggleButton->lifetime());
}
toggleButton->show();
toggleButton->setClickedCallback([=] {
createSelector(isEmoji ? Type::Stickers : Type::Emoji);
});
rpl::combine(
scroll->scrollTopValue(),
scroll->heightValue()
) | rpl::on_next([=](int scrollTop, int scrollHeight) {
const auto scrollBottom = scrollTop + scrollHeight;
selector.list->setVisibleTopBottom(scrollTop, scrollBottom);
}, selector.list->lifetime());
selector.list->scrollToRequests(
) | rpl::on_next([=](int y) {
scroll->scrollToY(y);
// _shadow->update();
}, selector.list->lifetime());
const auto separator = Ui::CreateChild<Ui::RpWidget>(container);
separator->paintRequest(
) | rpl::on_next([=](const QRect &r) {
auto p = QPainter(separator);
p.fillRect(r, st::shadowFg);
}, separator->lifetime());
selector.footer->show();
separator->show();
scroll->show();
const auto scrollWidth = stScroll.width;
sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto left = st::userpicBuilderEmojiSelectorLeft;
const auto mostTop = st::userpicBuilderEmojiSelectorLeft;
toggleButton->move(QPoint(left, mostTop));
selector.footer->setGeometry(
(isEmoji ? (rect::right(toggleButton) - togglePos.x()) : left),
mostTop,
s.width() - left,
selector.footer->height());
separator->setGeometry(
0,
rect::bottom(selector.footer),
s.width(),
st::lineWidth);
const auto listWidth = s.width() - st::boxRadius * 2;
selector.list->resizeToWidth(listWidth);
scroll->setGeometry(
st::boxRadius,
rect::bottom(separator),
selector.list->width() + scrollWidth,
s.height() - rect::bottom(separator));
selector.list->setMinimalHeight(listWidth, scroll->height());
}, lifetime());
// Reset all animations.
selector.list->hideFinished();
}
} // namespace
not_null<Ui::VerticalLayout*> CreateUserpicBuilder(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
StartData data,
BothWayCommunication<UserpicBuilder::Result> communication) {
const auto container = Ui::CreateChild<Ui::VerticalLayout>(parent.get());
struct State {
std::vector<not_null<CircleButton*>> circleButtons;
Ui::Animations::Simple chosenColorAnimation;
int colorIndex = -1;
std::vector<QColor> editorColors;
StartData gradientEditorStartData;
};
const auto state = container->lifetime().make_state<State>();
const auto preview = container->add(
object_ptr<EmojiUserpic>(
container,
Size(st::settingsInfoPhotoSize),
data.isForum),
st::userpicBuilderEmojiPreviewPadding,
style::al_top);
if (const auto id = data.documentId) {
const auto document = controller->session().data().document(id);
if (document && document->sticker()) {
preview->setDocument(document);
}
}
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_userpic_builder_color_subtitle(),
st::userpicBuilderEmojiSubtitle),
st::userpicBuilderEmojiSubtitlePadding,
style::al_top);
const auto paletteBg = Ui::AddBubbleWrap(
container,
QSize(
st::userpicBuilderEmojiBubblePaletteWidth,
std::abs(Ui::BubbleWrapInnerRect(QRect(0, 0, 0, 0)).height())
+ st::userpicBuilderEmojiAccentColorSize
+ rect::m::sum::v(
st::userpicBuilderEmojiBubblePalettePadding)));
const auto palette = Ui::CreateChild<Ui::VerticalLayout>(paletteBg.get());
{
constexpr auto kColorsCount = int(7);
const auto checkIsSpecial = [=](int i) {
return (i == kColorsCount);
};
const auto size = st::userpicBuilderEmojiAccentColorSize;
const auto paletteGradients = PaletteGradients();
for (auto i = 0; i < kColorsCount + 1; i++) {
const auto isSpecial = checkIsSpecial(i);
const auto colors = paletteGradients[i % kColorsCount];
const auto button = Ui::CreateChild<CircleButton>(palette);
state->circleButtons.push_back(button);
button->resize(size, size);
button->setBrush(isSpecial
? GenerateSpecial(size, state->editorColors)
: GenerateGradient(Size(size), colors));
const auto openEditor = isSpecial
? Fn<void()>([=] {
if (checkIsSpecial(state->colorIndex)) {
state->colorIndex = -1;
}
ShowGradientEditor(
controller,
state->gradientEditorStartData,
[=](std::vector<QColor> colors) {
state->editorColors = std::move(colors);
button->setBrush(
GenerateSpecial(size, state->editorColors));
button->clicked({}, Qt::LeftButton);
});
})
: nullptr;
button->setClickedCallback([=] {
if (openEditor && state->editorColors.empty()) {
return openEditor();
}
const auto was = state->colorIndex;
const auto now = i;
if (was == now) {
if (openEditor) {
openEditor();
}
return;
}
state->chosenColorAnimation.stop();
state->chosenColorAnimation.start([=](float64 progress) {
if (was >= 0) {
state->circleButtons[was]->setSelectedProgress(
1. - progress);
}
state->circleButtons[now]->setSelectedProgress(progress);
}, 0., 1., st::universalDuration);
state->colorIndex = now;
const auto result = isSpecial
? state->editorColors
: colors;
state->gradientEditorStartData.gradientEditorColors = result;
preview->setGradientColors(result);
});
}
const auto current = data.builderColorIndex % kColorsCount;
state->circleButtons[current]->setSelectedProgress(1.);
state->circleButtons[current]->clicked({}, Qt::LeftButton);
}
paletteBg->sizeValue(
) | rpl::on_next([=](const QSize &s) {
palette->setGeometry(Ui::BubbleWrapInnerRect(Rect(s))
- st::userpicBuilderEmojiBubblePalettePadding);
AlignChildren(palette, palette->width());
}, palette->lifetime());
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_userpic_builder_emoji_subtitle(),
st::userpicBuilderEmojiSubtitle),
st::userpicBuilderEmojiSubtitlePadding,
style::al_top);
const auto selectorBg = Ui::AddBubbleWrap(
container,
QSize(
st::userpicBuilderEmojiBubblePaletteWidth,
st::userpicBuilderEmojiSelectorMinHeight));
const auto selector = Ui::CreateChild<EmojiSelector>(
selectorBg.get(),
controller,
base::take(data.documents));
selector->chosen(
) | rpl::on_next([=](not_null<DocumentData*> document) {
state->gradientEditorStartData.documentId = document->id;
preview->setDocument(document);
}, preview->lifetime());
selectorBg->sizeValue(
) | rpl::on_next([=](const QSize &s) {
selector->setGeometry(Ui::BubbleWrapInnerRect(Rect(s)));
}, selector->lifetime());
base::take(
communication.triggers
) | rpl::on_next([=, done = base::take(communication.result)] {
preview->result(Editor::kProfilePhotoSize, [=](Result result) {
done(std::move(result));
});
}, preview->lifetime());
return container;
}
not_null<Ui::RpWidget*> CreateEmojiUserpic(
not_null<Ui::RpWidget*> parent,
const QSize &size,
rpl::producer<not_null<DocumentData*>> document,
rpl::producer<int> colorIndex,
bool isForum) {
const auto paletteGradients = PaletteGradients();
const auto widget = Ui::CreateChild<EmojiUserpic>(
parent.get(),
size,
isForum);
std::move(
document
) | rpl::on_next([=](not_null<DocumentData*> d) {
widget->setDocument(d);
}, widget->lifetime());
std::move(
colorIndex
) | rpl::on_next([=](int index) {
widget->setGradientColors(
paletteGradients[index % paletteGradients.size()]);
}, widget->lifetime());
return widget;
}
} // namespace UserpicBuilder

View File

@@ -0,0 +1,40 @@
/*
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 RpWidget;
class VerticalLayout;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace UserpicBuilder {
struct StartData;
struct Result;
template <typename Result>
struct BothWayCommunication;
not_null<Ui::VerticalLayout*> CreateUserpicBuilder(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
StartData data,
BothWayCommunication<UserpicBuilder::Result> communication);
[[nodiscard]] not_null<Ui::RpWidget*> CreateEmojiUserpic(
not_null<Ui::RpWidget*> parent,
const QSize &size,
rpl::producer<not_null<DocumentData*>> document,
rpl::producer<int> colorIndex,
bool isForum);
} // namespace UserpicBuilder