diff --git a/ui/basic.style b/ui/basic.style index 9d0a66a..6c49f39 100644 --- a/ui/basic.style +++ b/ui/basic.style @@ -268,17 +268,6 @@ inlineRowFileDescriptionTop: 23px; inlineResultsMinWidth: 48px; inlineDurationMargin: 3px; -toastTextStyle: defaultTextStyle; -toastTextPalette: TextPalette(defaultTextPalette) { - linkFg: mediaviewTextLinkFg; - monoFg: mediaviewCaptionFg; -} -toastMaxWidth: 480px; -toastMinMargin: 13px; -toastPadding: margins(19px, 13px, 19px, 12px); -toastFadeInDuration: 200; -toastFadeOutDuration: 1000; - historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }}; historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }}; boxTitleCloseIcon: icon {{ "box_button_close", boxTitleCloseFg }}; diff --git a/ui/emoji_config.cpp b/ui/emoji_config.cpp index ac96452..38eacbc 100644 --- a/ui/emoji_config.cpp +++ b/ui/emoji_config.cpp @@ -32,7 +32,7 @@ constexpr auto kImagesPerRow = 32; constexpr auto kImageRowsPerSprite = 16; constexpr auto kSetVersion = uint32(1); -constexpr auto kCacheVersion = uint32(3); +constexpr auto kCacheVersion = uint32(4); constexpr auto kMaxId = uint32(1 << 8); constexpr auto kScaleForTouchBar = 150; diff --git a/ui/layers/layers.style b/ui/layers/layers.style index 0b22d9b..ddb32ea 100644 --- a/ui/layers/layers.style +++ b/ui/layers/layers.style @@ -85,13 +85,13 @@ boxTitle: FlatLabel(defaultFlatLabel) { linkFontOver: font(17px semibold underline); } } -boxTitlePosition: point(23px, 16px); +boxTitlePosition: point(22px, 16px); boxTitleHeight: 56px; boxTitleAdditionalSkip: 9px; boxTitleAdditionalFont: normalFont; boxScroll: defaultSolidScroll; -boxRowPadding: margins(23px, 0px, 23px, 0px); +boxRowPadding: margins(22px, 0px, 22px, 0px); boxTopMargin: 6px; @@ -121,7 +121,7 @@ boxOptionInputSkip: 6px; boxVerticalMargin: 10px; boxWidth: 320px; boxWideWidth: 364px; -boxPadding: margins(23px, 30px, 23px, 8px); +boxPadding: margins(22px, 30px, 22px, 8px); boxMaxListHeight: 492px; boxLittleSkip: 10px; boxMediumSkip: 20px; diff --git a/ui/toast/toast.cpp b/ui/toast/toast.cpp index 8c5075b..b2016ba 100644 --- a/ui/toast/toast.cpp +++ b/ui/toast/toast.cpp @@ -8,6 +8,7 @@ #include "ui/toast/toast_manager.h" #include "ui/toast/toast_widget.h" +#include "styles/style_widgets.h" namespace Ui { namespace Toast { @@ -21,49 +22,51 @@ Instance::Instance( const Config &config, not_null widgetParent, const Private &) -: _hideAtMs(crl::now() + config.durationMs) { - _widget = std::make_unique(widgetParent, config); - _a_opacity.start( - [=] { opacityAnimationCallback(); }, +: _st(config.st) +, _hideAt(crl::now() + config.durationMs) +, _sliding(config.slideSide != RectPart::None) +, _widget(std::make_unique(widgetParent, config)) { + _shownAnimation.start( + [=] { shownAnimationCallback(); }, 0., 1., - st::toastFadeInDuration); + _sliding ? _st->durationSlide : _st->durationFadeIn); } void SetDefaultParent(not_null parent) { DefaultParent = parent; } -void Show(not_null parent, const Config &config) { +base::weak_ptr Show( + not_null parent, + const Config &config) { const auto manager = internal::Manager::instance(parent); - manager->addToast(std::make_unique( + return manager->addToast(std::make_unique( config, parent, Instance::Private())); } -void Show(const Config &config) { +base::weak_ptr Show(const Config &config) { if (const auto parent = DefaultParent.data()) { - Show(parent, config); + return Show(parent, config); } + return nullptr; } -void Show(not_null parent, const QString &text) { - auto config = Config(); - config.text = { text }; - Show(parent, config); +base::weak_ptr Show( + not_null parent, + const QString &text) { + return Show(parent, Config{ .text = { text }, .st = &st::defaultToast }); } -void Show(const QString &text) { - auto config = Config(); - config.text = { text }; - Show(config); +base::weak_ptr Show(const QString &text) { + return Show(Config{ .text = { text }, .st = &st::defaultToast }); } -void Instance::opacityAnimationCallback() { - _widget->setShownLevel(_a_opacity.value(_hiding ? 0. : 1.)); - _widget->update(); - if (!_a_opacity.animating()) { +void Instance::shownAnimationCallback() { + _widget->setShownLevel(_shownAnimation.value(_hiding ? 0. : 1.)); + if (!_shownAnimation.animating()) { if (_hiding) { hide(); } @@ -72,7 +75,11 @@ void Instance::opacityAnimationCallback() { void Instance::hideAnimated() { _hiding = true; - _a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., st::toastFadeOutDuration); + _shownAnimation.start( + [=] { shownAnimationCallback(); }, + 1., + 0., + _sliding ? _st->durationSlide : _st->durationFadeOut); } void Instance::hide() { @@ -80,5 +87,9 @@ void Instance::hide() { _widget->deleteLater(); } +not_null Instance::widget() const { + return _widget.get(); +} + } // namespace Toast } // namespace Ui diff --git a/ui/toast/toast.h b/ui/toast/toast.h index b0e0198..6c2a67c 100644 --- a/ui/toast/toast.h +++ b/ui/toast/toast.h @@ -9,6 +9,13 @@ #include "ui/effects/animations.h" #include "ui/text/text_entity.h" #include "ui/click_handler.h" +#include "ui/rect_part.h" +#include "ui/rp_widget.h" +#include "base/weak_ptr.h" + +namespace style { +struct Toast; +} // namespace style namespace Ui { namespace Toast { @@ -23,22 +30,18 @@ using ClickHandlerFilter = Fn; inline constexpr auto kDefaultDuration = crl::time(1500); struct Config { TextWithEntities text; - QMargins padding; + not_null st; crl::time durationMs = kDefaultDuration; - int minWidth = 0; - int maxWidth = 0; int maxLines = 16; bool multiline = false; bool dark = false; + RectPart slideSide = RectPart::None; ClickHandlerFilter filter; }; -void SetDefaultParent(not_null parent); -void Show(not_null parent, const Config &config); -void Show(const Config &config); -void Show(not_null parent, const QString &text); -void Show(const QString &text); -class Instance { +void SetDefaultParent(not_null parent); + +class Instance final : public base::has_weak_ptr { struct Private { }; @@ -53,20 +56,35 @@ public: void hideAnimated(); void hide(); + [[nodiscard]] not_null widget() const; + private: - void opacityAnimationCallback(); + void shownAnimationCallback(); + const not_null _st; + const crl::time _hideAt = 0; + + Ui::Animations::Simple _shownAnimation; bool _hiding = false; - Ui::Animations::Simple _a_opacity; - - const crl::time _hideAtMs; + bool _sliding = false; // ToastManager should reset _widget pointer if _widget is destroyed. friend class internal::Manager; - friend void Show(not_null parent, const Config &config); + friend base::weak_ptr Show( + not_null parent, + const Config &config); std::unique_ptr _widget; }; +base::weak_ptr Show( + not_null parent, + const Config &config); +base::weak_ptr Show(const Config &config); +base::weak_ptr Show( + not_null parent, + const QString &text); +base::weak_ptr Show(const QString &text); + } // namespace Toast } // namespace Ui diff --git a/ui/toast/toast_manager.cpp b/ui/toast/toast_manager.cpp index adbac76..165af03 100644 --- a/ui/toast/toast_manager.cpp +++ b/ui/toast/toast_manager.cpp @@ -44,13 +44,16 @@ not_null Manager::instance(not_null parent) { return i->second; } -void Manager::addToast(std::unique_ptr &&toast) { +base::weak_ptr Manager::addToast( + std::unique_ptr &&toast) { _toasts.push_back(std::move(toast)); const auto t = _toasts.back().get(); const auto widget = t->_widget.get(); _toastByWidget.emplace(widget, t); - connect(widget, SIGNAL(destroyed(QObject*)), this, SLOT(onToastWidgetDestroyed(QObject*))); + connect(widget, &QObject::destroyed, [=] { + toastWidgetDestroyed(widget); + }); if (const auto parent = widget->parentWidget()) { auto found = false; for (auto i = _toastParents.begin(); i != _toastParents.cend();) { @@ -72,10 +75,11 @@ void Manager::addToast(std::unique_ptr &&toast) { const auto nearestHide = _toastByHideTime.empty() ? 0LL : _toastByHideTime.begin()->first; - _toastByHideTime.emplace(t->_hideAtMs, t); + _toastByHideTime.emplace(t->_hideAt, t); if (!nearestHide || _toastByHideTime.begin()->first < nearestHide) { startNextHideTimer(); } + return make_weak(t); } void Manager::hideByTimer() { @@ -92,7 +96,7 @@ void Manager::hideByTimer() { startNextHideTimer(); } -void Manager::onToastWidgetDestroyed(QObject *widget) { +void Manager::toastWidgetDestroyed(QObject *widget) { const auto i = _toastByWidget.find(static_cast(widget)); if (i == _toastByWidget.cend()) { return; @@ -101,6 +105,13 @@ void Manager::onToastWidgetDestroyed(QObject *widget) { _toastByWidget.erase(i); toast->_widget.release(); + for (auto i = begin(_toastByHideTime); i != end(_toastByHideTime); ++i) { + if (i->second == toast) { + _toastByHideTime.erase(i); + break; + } + } + const auto j = ranges::find( _toasts, toast.get(), @@ -111,7 +122,9 @@ void Manager::onToastWidgetDestroyed(QObject *widget) { } void Manager::startNextHideTimer() { - if (_toastByHideTime.empty()) return; + if (_toastByHideTime.empty()) { + return; + } auto ms = crl::now(); if (ms >= _toastByHideTime.begin()->first) { diff --git a/ui/toast/toast_manager.h b/ui/toast/toast_manager.h index c094260..f60f22b 100644 --- a/ui/toast/toast_manager.h +++ b/ui/toast/toast_manager.h @@ -14,9 +14,7 @@ namespace Toast { namespace internal { class Widget; -class Manager : public QObject { - Q_OBJECT - +class Manager final : public QObject { struct CreateTag { }; @@ -28,20 +26,17 @@ public: static not_null instance(not_null parent); - void addToast(std::unique_ptr &&toast); + base::weak_ptr addToast(std::unique_ptr &&toast); protected: bool eventFilter(QObject *o, QEvent *e); -private slots: - void onToastWidgetDestroyed(QObject *widget); - private: + void toastWidgetDestroyed(QObject *widget); void startNextHideTimer(); void hideByTimer(); base::Timer _hideTimer; - crl::time _nextHide = 0; base::flat_multi_map> _toastByHideTime; base::flat_map, not_null> _toastByWidget; diff --git a/ui/toast/toast_widget.cpp b/ui/toast/toast_widget.cpp index 933782c..3f8f2a0 100644 --- a/ui/toast/toast_widget.cpp +++ b/ui/toast/toast_widget.cpp @@ -8,6 +8,7 @@ #include "ui/image/image_prepare.h" #include "styles/palette.h" +#include "styles/style_widgets.h" #include @@ -16,16 +17,16 @@ namespace Toast { namespace internal { Widget::Widget(QWidget *parent, const Config &config) -: TWidget(parent) +: RpWidget(parent) +, _st(config.st) , _roundRect(ImageRoundRadius::Large, st::toastBg) +, _slideSide(config.slideSide) , _multiline(config.multiline) , _dark(config.dark) -, _maxWidth((config.maxWidth > 0) ? config.maxWidth : st::toastMaxWidth) -, _padding((config.padding.left() > 0) ? config.padding : st::toastPadding) -, _maxTextWidth(widthWithoutPadding(_maxWidth)) +, _maxTextWidth(widthWithoutPadding(_st->maxWidth)) , _maxTextHeight( - st::toastTextStyle.font->height * (_multiline ? config.maxLines : 1)) -, _text(_multiline ? widthWithoutPadding(config.minWidth) : QFIXED_MAX) + config.st->style.font->height * (_multiline ? config.maxLines : 1)) +, _text(_multiline ? widthWithoutPadding(config.st->minWidth) : QFIXED_MAX) , _clickHandlerFilter(config.filter) { const auto toastOptions = TextParseOptions{ TextParseMultiline, @@ -34,7 +35,7 @@ Widget::Widget(QWidget *parent, const Config &config) Qt::LayoutDirectionAuto }; _text.setMarkedText( - st::toastTextStyle, + _st->style, _multiline ? config.text : TextUtilities::SingleLine(config.text), toastOptions); @@ -49,36 +50,102 @@ Widget::Widget(QWidget *parent, const Config &config) } void Widget::onParentResized() { - auto newWidth = _maxWidth; - accumulate_min(newWidth, _padding.left() + _text.maxWidth() + _padding.right()); - accumulate_min(newWidth, parentWidget()->width() - 2 * st::toastMinMargin); - _textWidth = widthWithoutPadding(newWidth); - const auto textHeight = _multiline + updateGeometry(); +} + +void Widget::updateGeometry() { + auto width = _st->maxWidth; + accumulate_min( + width, + _st->padding.left() + _text.maxWidth() + _st->padding.right()); + accumulate_min( + width, + parentWidget()->width() - _st->margin.left() - _st->margin.right()); + _textWidth = widthWithoutPadding(width); + _textHeight = _multiline ? qMin(_text.countHeight(_textWidth), _maxTextHeight) : _text.minHeight(); - const auto newHeight = _padding.top() + textHeight + _padding.bottom(); - setGeometry((parentWidget()->width() - newWidth) / 2, (parentWidget()->height() - newHeight) / 2, newWidth, newHeight); + const auto minHeight = _st->icon.empty() + ? 0 + : (_st->icon.height() + 2 * _st->iconPosition.y()); + const auto normalHeight = _st->padding.top() + + _textHeight + + _st->padding.bottom(); + const auto height = std::max(minHeight, normalHeight); + _textTop = _st->padding.top() + ((height - normalHeight) / 2); + const auto rect = QRect(0, 0, width, height); + const auto outer = parentWidget()->size(); + const auto full = QPoint(outer.width(), outer.height()); + const auto middle = QPoint( + (outer.width() - width) / 2, + (outer.height() - height) / 2); + const auto interpolated = [&](int from, int to) { + return anim::interpolate(from, to, _shownLevel); + }; + setGeometry(rect.translated([&] { + switch (_slideSide) { + case RectPart::None: + return middle; + case RectPart::Left: + return QPoint( + interpolated(-width, _st->margin.left()), + middle.y()); + case RectPart::Top: + return QPoint( + middle.x(), + interpolated(-height, _st->margin.top())); + case RectPart::Right: + return QPoint( + full.x() - interpolated(0, width + _st->margin.right()), + middle.y()); + case RectPart::Bottom: + return QPoint( + middle.x(), + full.y() - interpolated(0, height + _st->margin.bottom())); + } + Unexpected("Slide side in Toast::Widget::updateGeometry."); + }())); } void Widget::setShownLevel(float64 shownLevel) { _shownLevel = shownLevel; + if (_slideSide != RectPart::None) { + updateGeometry(); + } else { + update(); + } } void Widget::paintEvent(QPaintEvent *e) { Painter p(this); PainterHighQualityEnabler hq(p); - p.setOpacity(_shownLevel); + if (_slideSide == RectPart::None) { + p.setOpacity(_shownLevel); + } _roundRect.paint(p, rect()); if (_dark) { _roundRect.paint(p, rect()); } - p.setTextPalette(st::toastTextPalette); + if (!_st->icon.empty()) { + _st->icon.paint( + p, + _st->iconPosition.x(), + _st->iconPosition.y(), + width()); + } - const auto lines = _maxTextHeight / st::toastTextStyle.font->height; + p.setTextPalette(_st->palette); + + const auto lines = _maxTextHeight / _st->style.font->height; p.setPen(st::toastFg); - _text.drawElided(p, _padding.left(), _padding.top(), _textWidth + 1, lines); + _text.drawElided( + p, + _st->padding.left(), + _textTop, + _textWidth + 1, + lines); } void Widget::leaveEventHook(QEvent *e) { @@ -96,8 +163,9 @@ void Widget::mouseMoveEvent(QMouseEvent *e) { if (!_text.hasLinks()) { return; } - const auto point = e->pos() - QPoint(_padding.left(), _padding.top()); - const auto lines = _maxTextHeight / st::toastTextStyle.font->height; + const auto point = e->pos() + - QPoint(_st->padding.left(), _textTop); + const auto lines = _maxTextHeight / _st->style.font->height; const auto state = _text.getStateElided(point, _textWidth + 1); const auto was = ClickHandler::getActive(); if (was != state.link) { @@ -129,6 +197,10 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { } } +int Widget::widthWithoutPadding(int w) const { + return w - _st->padding.left() - _st->padding.right(); +} + } // namespace internal } // namespace Toast } // namespace Ui diff --git a/ui/toast/toast_widget.h b/ui/toast/toast_widget.h index 22b6db8..1ec5e97 100644 --- a/ui/toast/toast_widget.h +++ b/ui/toast/toast_widget.h @@ -15,7 +15,7 @@ namespace Ui { namespace Toast { namespace internal { -class Widget : public TWidget { +class Widget final : public RpWidget { public: Widget(QWidget *parent, const Config &config); @@ -33,21 +33,22 @@ protected: void mouseReleaseEvent(QMouseEvent *e) override; private: - int widthWithoutPadding(int w) { - return w - _padding.left() - _padding.right(); - } + [[nodiscard]] int widthWithoutPadding(int w) const; + void updateGeometry(); + const not_null _st; RoundRect _roundRect; + RectPart _slideSide = RectPart::None; float64 _shownLevel = 0; bool _multiline = false; bool _dark = false; - int _maxWidth = 0; - QMargins _padding; int _maxTextWidth = 0; int _maxTextHeight = 0; int _textWidth = 0; + int _textHeight = 0; + int _textTop = 0; Text::String _text; ClickHandlerFilter _clickHandlerFilter; diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index e6e9fae..b39af53 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -568,6 +568,20 @@ SideBarButton { ripple: RippleAnimation; } +Toast { + style: TextStyle; + icon: icon; + iconPosition: point; + palette: TextPalette; + padding: margins; + margin: margins; + minWidth: pixels; + maxWidth: pixels; + durationFadeIn: int; + durationFadeOut: int; + durationSlide: int; +} + defaultLabelSimple: LabelSimple { font: normalFont; maxWidth: 0px; @@ -1248,7 +1262,7 @@ defaultSettingsButton: SettingsButton { toggle: defaultSettingsToggle; toggleOver: defaultSettingsToggleOver; - toggleSkip: 23px; + toggleSkip: 22px; ripple: defaultRippleAnimation; } @@ -1266,6 +1280,20 @@ defaultSideBarButton: SideBarButton { } } +defaultToast: Toast { + style: defaultTextStyle; + palette: TextPalette(defaultTextPalette) { + linkFg: mediaviewTextLinkFg; + monoFg: mediaviewCaptionFg; + } + padding: margins(19px, 13px, 19px, 12px); + margin: margins(13px, 13px, 13px, 13px); + maxWidth: 480px; + durationFadeIn: 200; + durationFadeOut: 1000; + durationSlide: 160; +} + // Windows specific title windowTitleButton: IconButton {