Improved style of emoji picker overlay with shadow and tail.

This commit is contained in:
23rd
2026-04-13 21:49:55 +03:00
parent 7b720b2a3f
commit 9c21d99e01
4 changed files with 165 additions and 42 deletions
@@ -212,23 +212,25 @@ void CreateStickerBox(
.maxSelected = kMaxEmojis,
.allowExpand = true,
};
const auto pickerCollapsed = [&] {
auto probe = ChatHelpers::EmojiPickerOverlay(nullptr, pickerDescriptor);
return probe.collapsedHeight();
}();
const auto pickerExpanded = [&] {
auto probe = ChatHelpers::EmojiPickerOverlay(nullptr, pickerDescriptor);
return probe.expandedHeight();
}();
const auto pickerExtra = pickerExpanded - pickerCollapsed;
const auto metrics = ChatHelpers::EmojiPickerOverlay::EstimateMetrics(
pickerDescriptor.aboutText);
const auto pickerCollapsed = metrics.collapsedHeight;
const auto pickerTotalExpanded = metrics.totalExpandedHeight;
const auto shadowExt = metrics.shadowExtent;
constexpr auto kStickerOverlap = 24;
const auto stickerTop = shadowExt.top()
+ pickerCollapsed
- kStickerOverlap;
const auto holderHeight = std::max(
stickerTop + kPreviewSide,
pickerTotalExpanded);
const auto previewHolder = inner->add(
object_ptr<Ui::RpWidget>(inner),
QMargins(0, 0, 0, 0),
style::al_top);
previewHolder->resize(
st::boxWideWidth,
kPreviewSide + pickerExtra);
previewHolder->resize(st::boxWideWidth, holderHeight);
const auto preview = Ui::CreateChild<PreviewWidget>(
previewHolder,
image);
@@ -238,18 +240,20 @@ void CreateStickerBox(
std::move(pickerDescriptor));
auto layoutOverlay = [=] {
const auto w = std::min(
previewHolder->width() - 2 * st::boxRowPadding.left(),
const auto bubbleW = std::min(
previewHolder->width()
- 2 * st::boxRowPadding.left()
- shadowExt.left() - shadowExt.right(),
int(kPreviewSide * 1.1));
const auto x = (previewHolder->width() - w) / 2;
const auto y = kPreviewSide - pickerCollapsed;
picker->setGeometry(x, y, w, pickerExpanded);
const auto totalW = bubbleW + shadowExt.left() + shadowExt.right();
const auto x = (previewHolder->width() - totalW) / 2;
picker->setGeometry(x, 0, totalW, pickerTotalExpanded);
picker->raise();
};
previewHolder->widthValue(
) | rpl::on_next([=](int width) {
preview->move((width - kPreviewSide) / 2, 0);
preview->move((width - kPreviewSide) / 2, stickerTop);
layoutOverlay();
}, preview->lifetime());
@@ -428,14 +428,19 @@ stickersAddCellPlusSize: 22px;
stickersAddCellPlusThickness: 2px;
stickersAddCellBgRadius: 28px;
stickersEmojiPickerRadius: 16px;
stickersEmojiPickerExpandedRadius: 20px;
stickersEmojiPickerBg: emojiPanBg;
stickersEmojiPickerShadow: windowShadowFg;
stickersEmojiPickerPadding: margins(12px, 10px, 12px, 10px);
stickersEmojiPickerPadding: margins(12px, 8px, 12px, 0px);
stickersEmojiPickerItemSize: 30px;
stickersEmojiPickerItemSkip: 4px;
stickersEmojiPickerStripHeight: 36px;
stickersEmojiPickerStripHeight: 40px;
stickersEmojiPickerExpandedHeight: 220px;
stickersEmojiPickerStripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg },
{ "chat/reactions_bubble", emojiPanBg },
};
stickersEmojiPickerStripBubbleRight: 20px;
stickersEmojiPickerSelectedBg: windowBgActive;
stickersEmojiPickerSelectedFg: windowBgActive;
stickersEmojiPickerHeaderFg: windowSubTextFg;
@@ -458,6 +463,11 @@ stickersEmojiPickerSectionHeader: FlatLabel(defaultFlatLabel) {
stickersEmojiPickerExpandIcon: icon {{ "intro_country_dropdown", windowSubTextFg }};
stickersEmojiPickerCollapseIcon: icon {{ "intro_country_dropdown-flip_vertical", windowSubTextFg }};
stickersEmojiPickerExpandSize: 24px;
stickersEmojiPickerBoxShadow: BoxShadow {
blurRadius: 20px;
offset: point(0px, 6px);
opacity: 0.22;
}
emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }};
@@ -34,6 +34,11 @@ namespace {
return result;
}
[[nodiscard]] std::vector<EmojiPtr> DefaultRecentVector() {
const auto src = Ui::Emoji::GetDefaultRecent();
return std::vector<EmojiPtr>(src.begin(), src.end());
}
} // namespace
class EmojiPickerOverlay::Strip final : public Ui::RpWidget {
@@ -354,19 +359,47 @@ void EmojiPickerOverlay::Grid::leaveEventHook(QEvent *e) {
updateHover(-1);
}
EmojiPickerOverlay::Metrics EmojiPickerOverlay::EstimateMetrics(
const QString &aboutText) {
const auto tailHeight = st::stickersEmojiPickerStripBubble.height();
const auto shadowExtent = Ui::BoxShadow::ExtendFor(
st::stickersEmojiPickerBoxShadow);
const auto &pad = st::stickersEmojiPickerPadding;
auto about = Ui::FlatLabel(
nullptr,
aboutText,
st::stickersEmojiPickerAbout);
const auto collapsedHeight = pad.top()
+ about.height()
+ st::stickersEmojiPickerStripHeight
+ pad.bottom();
const auto expandedHeight = collapsedHeight
+ st::stickersEmojiPickerExpandedHeight;
const auto shadowAndTail = shadowExtent.top()
+ shadowExtent.bottom()
+ tailHeight;
return {
.shadowExtent = shadowExtent,
.tailHeight = tailHeight,
.collapsedHeight = collapsedHeight,
.expandedHeight = expandedHeight,
.totalCollapsedHeight = collapsedHeight + shadowAndTail,
.totalExpandedHeight = expandedHeight + shadowAndTail,
};
}
EmojiPickerOverlay::EmojiPickerOverlay(
QWidget *parent,
EmojiPickerOverlayDescriptor descriptor)
: RpWidget(parent)
, _aboutText(std::move(descriptor.aboutText))
, _recent(descriptor.recent.empty()
? std::vector<EmojiPtr>(
Ui::Emoji::GetDefaultRecent().begin(),
Ui::Emoji::GetDefaultRecent().end())
? DefaultRecentVector()
: std::move(descriptor.recent))
, _maxSelected(descriptor.maxSelected)
, _allowExpand(descriptor.allowExpand)
, _selectedList(std::move(descriptor.initialSelected)) {
, _selectedList(std::move(descriptor.initialSelected))
, _shadow(st::stickersEmojiPickerBoxShadow) {
_allForGrid = BuildAllEmojis();
_about = std::make_unique<Ui::FlatLabel>(
@@ -405,7 +438,35 @@ EmojiPickerOverlay::EmojiPickerOverlay(
}
_selectedVar = _selectedList;
resize(width(), expandedHeight());
resize(width(), totalExpandedHeight());
}
QMargins EmojiPickerOverlay::shadowExtent() const {
return _shadow.extend();
}
int EmojiPickerOverlay::totalCollapsedHeight() const {
const auto ext = _shadow.extend();
return collapsedHeight() + ext.top() + ext.bottom() + tailHeight();
}
int EmojiPickerOverlay::totalExpandedHeight() const {
const auto ext = _shadow.extend();
return expandedHeight() + ext.top() + ext.bottom() + tailHeight();
}
QRect EmojiPickerOverlay::bubbleRect() const {
const auto ext = _shadow.extend();
return QRect(
ext.left(),
ext.top(),
width() - ext.left() - ext.right(),
height() - ext.top() - ext.bottom() - tailHeight());
}
QRect EmojiPickerOverlay::bubbleShownRect() const {
const auto r = bubbleRect();
return QRect(r.x(), r.y(), r.width(), currentShownHeight());
}
EmojiPickerOverlay::~EmojiPickerOverlay() = default;
@@ -455,8 +516,6 @@ int EmojiPickerOverlay::currentShownHeight() const {
}
void EmojiPickerOverlay::applyExpandProgress() {
const auto h = currentShownHeight();
setMask(QRegion(0, 0, width(), h));
if (_scroll) {
const auto progress = currentExpandValue();
_scroll->setVisible(progress > 0.);
@@ -489,33 +548,63 @@ int EmojiPickerOverlay::expandedHeight() const {
void EmojiPickerOverlay::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto progress = currentExpandValue();
const auto shown = bubbleShownRect();
const auto radius = st::stickersEmojiPickerExpandedRadius;
_shadow.paint(p, shown, radius);
p.setPen(Qt::NoPen);
p.setBrush(st::stickersEmojiPickerBg);
const auto radius = st::stickersEmojiPickerRadius;
const auto h = currentShownHeight();
p.drawRoundedRect(QRect(0, 0, width(), h), radius, radius);
p.drawRoundedRect(shown, radius, radius);
if (progress < 1.) {
paintTailBubble(p, shown, 1. - progress);
}
}
void EmojiPickerOverlay::paintTailBubble(
QPainter &p,
const QRect &bubble,
float64 opacity) {
const auto &icon = st::stickersEmojiPickerStripBubble;
const auto offsetRight = st::stickersEmojiPickerStripBubbleRight;
const auto x = bubble.right() + 1 - offsetRight - icon.width();
const auto y = bubble.bottom() + 1;
if (opacity >= 1.) {
icon.paint(p, x, y, width());
} else {
p.save();
p.setOpacity(opacity);
icon.paint(p, x, y, width());
p.restore();
}
}
void EmojiPickerOverlay::resizeEvent(QResizeEvent *e) {
setMask(QRegion(0, 0, width(), currentShownHeight()));
relayout();
}
void EmojiPickerOverlay::mousePressEvent(QMouseEvent *e) {
if (e->pos().y() > currentShownHeight()) {
if (!bubbleShownRect().contains(e->pos())) {
e->ignore();
}
}
int EmojiPickerOverlay::tailHeight() const {
return st::stickersEmojiPickerStripBubble.height();
}
void EmojiPickerOverlay::relayout() {
const auto &pad = st::stickersEmojiPickerPadding;
const auto bubble = bubbleRect();
const auto bubbleShown = currentShownHeight();
if (_about) {
_about->resizeToWidth(width() - pad.left() - pad.right());
_about->moveToLeft(pad.left(), pad.top());
_about->resizeToWidth(bubble.width() - pad.left() - pad.right());
_about->moveToLeft(bubble.left() + pad.left(), bubble.top() + pad.top());
}
const auto aboutBottom = _about
? (_about->y() + _about->height())
: pad.top();
: (bubble.top() + pad.top());
const auto stripTop = aboutBottom;
const auto stripH = st::stickersEmojiPickerStripHeight;
@@ -525,29 +614,30 @@ void EmojiPickerOverlay::relayout() {
const auto expandGap = _expandButton
? st::stickersEmojiPickerItemSkip
: 0;
const auto stripW = width()
const auto stripW = bubble.width()
- pad.left()
- pad.right()
- expandSize
- expandGap;
_strip->setGeometry(pad.left(), stripTop, stripW, stripH);
_strip->setGeometry(bubble.left() + pad.left(), stripTop, stripW, stripH);
_strip->refresh();
if (_expandButton) {
const auto bx = width() - pad.right() - expandSize;
const auto bx = bubble.right() + 1 - pad.right() - expandSize;
const auto by = stripTop + (stripH - expandSize) / 2;
_expandButton->moveToLeft(bx, by);
}
if (_scroll) {
const auto scrollTop = stripTop + stripH;
const auto bubbleBottom = bubble.top() + bubbleShown;
const auto scrollH = std::max(
0,
height() - scrollTop - pad.bottom());
bubbleBottom - scrollTop - pad.bottom());
_scroll->setGeometry(
pad.left(),
bubble.left() + pad.left(),
scrollTop,
width() - pad.left() - pad.right(),
bubble.width() - pad.left() - pad.right(),
scrollH);
if (_grid) {
_grid->resizeGetHeight(_scroll->width());
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "ui/emoji_config.h"
#include "ui/widgets/shadow.h"
namespace Ui {
class AbstractButton;
@@ -34,6 +35,16 @@ public:
EmojiPickerOverlayDescriptor descriptor);
~EmojiPickerOverlay();
struct Metrics {
QMargins shadowExtent;
int tailHeight = 0;
int collapsedHeight = 0;
int expandedHeight = 0;
int totalCollapsedHeight = 0;
int totalExpandedHeight = 0;
};
[[nodiscard]] static Metrics EstimateMetrics(const QString &aboutText);
[[nodiscard]] const std::vector<EmojiPtr> &selected() const;
[[nodiscard]] rpl::producer<std::vector<EmojiPtr>> selectedValue() const;
@@ -43,6 +54,9 @@ public:
[[nodiscard]] int collapsedHeight() const;
[[nodiscard]] int expandedHeight() const;
[[nodiscard]] QMargins shadowExtent() const;
[[nodiscard]] int totalCollapsedHeight() const;
[[nodiscard]] int totalExpandedHeight() const;
protected:
void paintEvent(QPaintEvent *e) override;
@@ -58,8 +72,12 @@ private:
void notifySelectionChanged();
void startExpandAnimation(bool expanded);
void applyExpandProgress();
void paintTailBubble(QPainter &p, const QRect &bubble, float64 opacity);
[[nodiscard]] float64 currentExpandValue() const;
[[nodiscard]] int currentShownHeight() const;
[[nodiscard]] int tailHeight() const;
[[nodiscard]] QRect bubbleRect() const;
[[nodiscard]] QRect bubbleShownRect() const;
const QString _aboutText;
const std::vector<EmojiPtr> _recent;
@@ -78,6 +96,7 @@ private:
std::unique_ptr<Ui::ScrollArea> _scroll;
Grid *_grid = nullptr;
Ui::Animations::Simple _expandAnim;
Ui::BoxShadow _shadow;
};