From b396d6e8363b152a598c4a8f343502c14e1648a4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 25 Dec 2021 20:01:49 +0000 Subject: [PATCH] Start from-reaction-button dropdown. --- .../history/history_inner_widget.cpp | 29 +- .../history/history_inner_widget.h | 3 +- .../history/view/history_view_element.cpp | 3 +- .../history/view/history_view_element.h | 3 +- .../history/view/history_view_message.cpp | 25 +- .../history/view/history_view_message.h | 3 +- .../view/history_view_react_button.cpp | 395 +++++++----------- .../history/view/history_view_react_button.h | 66 ++- Telegram/SourceFiles/ui/chat/chat.style | 11 +- 9 files changed, 207 insertions(+), 331 deletions(-) diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 8904c45afa..bbcf0204d3 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -179,7 +179,6 @@ HistoryInner::HistoryInner( [=] { update(); })) , _reactionsManager( std::make_unique( - historyWidget, [=](QRect updated) { update(updated); })) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) @@ -1540,7 +1539,6 @@ void HistoryInner::mouseActionFinish( .sessionWindow = base::make_weak(_controller.get()), }) }); - _reactionsManager->hideSelectors(anim::type::normal); return; } if ((_mouseAction == MouseAction::PrepareSelect) @@ -2677,7 +2675,7 @@ void HistoryInner::enterEventHook(QEnterEvent *e) { } void HistoryInner::leaveEventHook(QEvent *e) { - _reactionsManager->showButton({}); + _reactionsManager->updateButton({}); if (auto item = App::hoveredItem()) { repaintItem(item); App::hoveredItem(nullptr); @@ -2971,7 +2969,8 @@ void HistoryInner::onTouchSelect() { auto HistoryInner::reactionButtonParameters( not_null view, - QPoint position) const + QPoint position, + const HistoryView::TextState &reactionState) const -> HistoryView::Reactions::ButtonParameters { const auto top = itemTop(view); if (top < 0 @@ -2980,8 +2979,13 @@ auto HistoryInner::reactionButtonParameters( || inSelectionMode()) { return {}; } - const auto local = view->reactionButtonParameters(position); - return local.translated({ 0, itemTop(view) }); + auto result = view->reactionButtonParameters( + position, + reactionState + ).translated({ 0, itemTop(view) }); + result.visibleTop = _visibleAreaTop; + result.visibleBottom = _visibleAreaBottom; + return result; } void HistoryInner::mouseActionUpdate() { @@ -3018,7 +3022,10 @@ void HistoryInner::mouseActionUpdate() { } } m = mapPointToItem(point, view); - _reactionsManager->showButton(reactionButtonParameters(view, m)); + _reactionsManager->updateButton(reactionButtonParameters( + view, + m, + reactionState)); if (view->pointState(m) != PointState::Outside) { if (App::hoveredItem() != view) { repaintItem(App::hoveredItem()); @@ -3030,7 +3037,7 @@ void HistoryInner::mouseActionUpdate() { App::hoveredItem(nullptr); } } else { - _reactionsManager->showButton({}); + _reactionsManager->updateButton({}); } if (_mouseActionItem && !_mouseActionItem->mainView()) { mouseActionCancel(); @@ -3046,9 +3053,6 @@ void HistoryInner::mouseActionUpdate() { if (overReaction) { dragState = reactionState; lnkhost = reactionView; - _reactionsManager->showSelector([=](QPoint local) { - return mapToGlobal(local); - }); } else if (point.y() < _historyPaddingTop) { if (_botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) { dragState = TextState(nullptr, _botAbout->info->text.getState( @@ -3151,9 +3155,6 @@ void HistoryInner::mouseActionUpdate() { } } } - if (!overReaction) { - _reactionsManager->hideSelectors(anim::type::normal); - } auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost); if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 3dd0c34993..7cb32b98eb 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -352,7 +352,8 @@ private: HistoryView::Reactions::ButtonParameters reactionButtonParameters( not_null view, - QPoint position) const; + QPoint position, + const HistoryView::TextState &reactionState) const; void setupSharingDisallowed(); [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 6192b8f63c..93e9f12adc 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -993,7 +993,8 @@ TextSelection Element::adjustSelection( } Reactions::ButtonParameters Element::reactionButtonParameters( - QPoint position) const { + QPoint position, + const TextState &reactionState) const { return {}; } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index f1eff74e33..b902cf7363 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -323,7 +323,8 @@ public: TextSelectType type) const; [[nodiscard]] virtual auto reactionButtonParameters( - QPoint position) const -> Reactions::ButtonParameters; + QPoint position, + const TextState &reactionState) const -> Reactions::ButtonParameters; // ClickHandlerHost interface. void clickHandlerActiveChanged( diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index ff81dbea0d..5df9786245 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1805,22 +1805,21 @@ TextSelection Message::adjustSelection( } Reactions::ButtonParameters Message::reactionButtonParameters( - QPoint position) const { + QPoint position, + const TextState &reactionState) const { auto result = Reactions::ButtonParameters{ .context = data()->fullId() }; - result.outbg = hasOutLayout(); + const auto outbg = result.outbg = hasOutLayout(); const auto geometry = countGeometry(); result.pointer = position; - result.center = geometry.topLeft() - + QPoint(geometry.width(), geometry.height()) - + st::reactionCornerCenter; - const auto size = st::reactionCornerSize; - const auto button = QRect( - result.center - QPoint(size.width() / 2, size.height() / 2), - size); - result.active = button.marginsAdded( - st::reactionCornerActiveAreaPadding - ).contains(position); - if (!result.active) { + const auto onTheLeft = (outbg && !delegate()->elementIsChatWide()); + const auto leftAdd = onTheLeft ? 0 : geometry.width(); + result.center = geometry.topLeft() + (onTheLeft + ? (QPoint(0, geometry.height()) + QPoint( + -st::reactionCornerCenter.x(), + st::reactionCornerCenter.y())) + : (QPoint(geometry.width(), geometry.height()) + + st::reactionCornerCenter)); + if (reactionState.itemId != result.context) { const auto top = marginTop(); if (!QRect(0, top, width(), height() - top).contains(position)) { return {}; diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 18f0e4f6a1..5ec7a1c279 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -89,7 +89,8 @@ public: TextSelectType type) const override; Reactions::ButtonParameters reactionButtonParameters( - QPoint position) const override; + QPoint position, + const TextState &reactionState) const override; bool hasHeavyPart() const override; void unloadHeavyPart() override; diff --git a/Telegram/SourceFiles/history/view/history_view_react_button.cpp b/Telegram/SourceFiles/history/view/history_view_react_button.cpp index 9e482bc17f..8318016a96 100644 --- a/Telegram/SourceFiles/history/view/history_view_react_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_react_button.cpp @@ -22,6 +22,7 @@ namespace { constexpr auto kItemsPerRow = 5; constexpr auto kToggleDuration = crl::time(80); constexpr auto kActivateDuration = crl::time(150); +constexpr auto kExpandDuration = crl::time(150); constexpr auto kInCacheIndex = 0; constexpr auto kOutCacheIndex = 1; constexpr auto kShadowCacheIndex = 0; @@ -69,11 +70,10 @@ void CopyImagePart(QImage &to, const QImage &from, QRect source) { Button::Button( Fn update, ButtonParameters parameters) -: _update(std::move(update)) { - const auto initial = QRect(QPoint(), CountOuterSize()); - _geometry = initial.translated(parameters.center - initial.center()); - _outbg = parameters.outbg; - applyState(parameters.active ? State::Active : State::Shown); +: _update(std::move(update)) +, _collapsed(QPoint(), CountOuterSize()) +, _finalHeight(_collapsed.height()) { + applyParameters(parameters, nullptr); } Button::~Button() = default; @@ -90,24 +90,97 @@ QRect Button::geometry() const { return _geometry; } +bool Button::expandUp() const { + return (_expandDirection == ExpandDirection::Up); +} + void Button::applyParameters(ButtonParameters parameters) { - const auto geometry = _geometry.translated( - parameters.center - _geometry.center()); + applyParameters(std::move(parameters), _update); +} + +void Button::applyParameters( + ButtonParameters parameters, + Fn update) { + const auto shift = parameters.center - _collapsed.center(); + _collapsed = _collapsed.translated(shift); + updateGeometry(update); + const auto inner = _geometry.marginsRemoved(st::reactionCornerShadow); + const auto active = inner.marginsAdded( + st::reactionCornerActiveAreaPadding + ).contains(parameters.pointer); + const auto inside = inner.contains(parameters.pointer) + || (active && (_state == State::Inside)); + if (_state != State::Inside && !_heightAnimation.animating()) { + updateExpandDirection(parameters); + } + const auto state = inside + ? State::Inside + : active + ? State::Active + : State::Shown; + applyState(state, update); if (_outbg != parameters.outbg) { _outbg = parameters.outbg; - _update(_geometry); + if (update) { + update(_geometry); + } } +} + +void Button::updateExpandDirection(const ButtonParameters ¶meters) { + const auto maxAddedHeight = (parameters.reactionsCount - 1) + * (st::reactionCornerSize.height() + st::reactionCornerSkip); + const auto addedHeight = std::min( + maxAddedHeight, + st::reactionCornerAddedHeightMax); + _expandedHeight = _collapsed.height() + addedHeight; + if (parameters.reactionsCount < 2) { + return; + } + const auto up = (_collapsed.y() - addedHeight >= parameters.visibleTop) + || (_collapsed.y() + _collapsed.height() + addedHeight + > parameters.visibleBottom); + _expandDirection = up ? ExpandDirection::Up : ExpandDirection::Down; +} + +void Button::updateGeometry(Fn update) { + const auto added = int(base::SafeRound( + _heightAnimation.value(_finalHeight) + )) - _collapsed.height(); + const auto geometry = _collapsed.marginsAdded({ + 0, + (_expandDirection == ExpandDirection::Up) ? added : 0, + 0, + (_expandDirection == ExpandDirection::Down) ? added : 0, + }); if (_geometry != geometry) { - if (!_geometry.isNull()) { - _update(_geometry); + if (update) { + update(_geometry); } _geometry = geometry; - _update(_geometry); + if (update) { + update(_geometry); + } } - applyState(parameters.active ? State::Active : State::Shown); } void Button::applyState(State state) { + applyState(state, _update); +} + +void Button::applyState(State state, Fn update) { + const auto finalHeight = (state == State::Inside) + ? _expandedHeight + : _collapsed.height(); + if (_finalHeight != finalHeight) { + _heightAnimation.start( + [=] { updateGeometry(_update); }, + _finalHeight, + finalHeight, + kExpandDuration); + _finalHeight = finalHeight; + } + updateGeometry(update); if (_state == state) { return; } @@ -125,177 +198,26 @@ void Button::applyState(State state) { float64 Button::ScaleForState(State state) { switch (state) { - case State::Hidden: return 0.7; - case State::Shown: return 1.; - case State::Active: return 1.4; + case State::Hidden: return 0.5; + case State::Shown: return 0.7; + case State::Active: + case State::Inside: return 1.; } Unexpected("State in ReactionButton::ScaleForState."); } float64 Button::OpacityForScale(float64 scale) { - return (scale >= 1.) - ? 1. - : ((scale - ScaleForState(State::Hidden)) - / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))); + return std::max( + ((scale - ScaleForState(State::Hidden)) + / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))), + 1.); } float64 Button::currentScale() const { return _scaleAnimation.value(ScaleForState(_state)); } -Selector::Selector( - QWidget *parent, - const std::vector &list) -: _dropdown(parent) { - _dropdown.setAutoHiding(false); - - const auto content = _dropdown.setOwnedWidget( - object_ptr(&_dropdown)); - - const auto count = int(list.size()); - const auto single = st::reactionPopupImage; - const auto padding = st::reactionPopupPadding; - const auto width = padding.left() + single + padding.right(); - const auto height = padding.top() + single + padding.bottom(); - const auto rows = (count + kItemsPerRow - 1) / kItemsPerRow; - const auto columns = (int(list.size()) + rows - 1) / rows; - const auto inner = QRect(0, 0, columns * width, rows * height); - const auto outer = inner.marginsAdded(padding); - content->resize(outer.size()); - - _elements.reserve(list.size()); - auto x = padding.left(); - auto y = padding.top(); - auto row = -1; - auto perrow = 0; - while (_elements.size() != list.size()) { - if (!perrow) { - ++row; - perrow = (list.size() - _elements.size()) / (rows - row); - x = (outer.width() - perrow * width) / 2; - } - auto &reaction = list[_elements.size()]; - _elements.push_back({ - .emoji = reaction.emoji, - .geometry = QRect(x, y + row * height, width, height), - }); - x += width; - --perrow; - } - - struct State { - int selected = -1; - int pressed = -1; - }; - const auto state = content->lifetime().make_state(); - content->setMouseTracking(true); - content->events( - ) | rpl::start_with_next([=](not_null e) { - const auto type = e->type(); - if (type == QEvent::MouseMove) { - const auto position = static_cast(e.get())->pos(); - const auto i = ranges::find_if(_elements, [&](const Element &e) { - return e.geometry.contains(position); - }); - const auto selected = (i != end(_elements)) - ? int(i - begin(_elements)) - : -1; - if (state->selected != selected) { - state->selected = selected; - content->update(); - } - } else if (type == QEvent::MouseButtonPress) { - state->pressed = state->selected; - content->update(); - } else if (type == QEvent::MouseButtonRelease) { - const auto pressed = std::exchange(state->pressed, -1); - if (pressed >= 0) { - content->update(); - if (pressed == state->selected) { - _chosen.fire_copy(_elements[pressed].emoji); - } - } - } - }, content->lifetime()); - - content->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(content); - const auto radius = st::roundRadiusSmall; - { - auto hq = PainterHighQualityEnabler(p); - p.setBrush(st::emojiPanBg); - p.setPen(Qt::NoPen); - p.drawRoundedRect(content->rect(), radius, radius); - } - auto index = 0; - const auto activeIndex = (state->pressed >= 0) - ? state->pressed - : state->selected; - const auto realSize = Ui::Emoji::GetSizeNormal(); - const auto size = realSize / style::DevicePixelRatio(); - for (const auto &element : _elements) { - const auto active = (index++ == activeIndex); - if (active) { - auto hq = PainterHighQualityEnabler(p); - p.setBrush(st::windowBgOver); - p.setPen(Qt::NoPen); - p.drawRoundedRect(element.geometry, radius, radius); - } - if (const auto emoji = Ui::Emoji::Find(element.emoji)) { - Ui::Emoji::Draw( - p, - emoji, - realSize, - element.geometry.x() + (width - size) / 2, - element.geometry.y() + (height - size) / 2); - } - } - }, content->lifetime()); - - _dropdown.resizeToContent(); -} - -void Selector::showAround(QRect area) { - const auto parent = _dropdown.parentWidget(); - const auto left = std::min( - std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0), - parent->width() - _dropdown.width()); - _fromTop = (area.y() >= _dropdown.height()); - _fromLeft = (area.center().x() - left - <= left + _dropdown.width() - area.center().x()); - const auto top = _fromTop - ? (area.y() - _dropdown.height()) - : (area.y() + area.height()); - _dropdown.move(left, top); -} - -void Selector::toggle(bool shown, anim::type animated) { - if (animated == anim::type::normal) { - if (shown) { - using Origin = Ui::PanelAnimation::Origin; - _dropdown.showAnimated(_fromTop - ? (_fromLeft ? Origin::BottomLeft : Origin::BottomRight) - : (_fromLeft ? Origin::TopLeft : Origin::TopRight)); - } else { - _dropdown.hideAnimated(); - } - } else if (shown) { - _dropdown.showFast(); - } else { - _dropdown.hideFast(); - } -} - -[[nodiscard]] rpl::producer Selector::chosen() const { - return _chosen.events(); -} - -[[nodiscard]] rpl::lifetime &Selector::lifetime() { - return _dropdown.lifetime(); -} - -Manager::Manager(QWidget *selectorParent, Fn buttonUpdate) +Manager::Manager(Fn buttonUpdate) : _outer(CountOuterSize()) , _inner(QRectF({}, st::reactionCornerSize)) , _innerActive(QRect({}, CountMaxSizeWithMargins({}))) @@ -307,8 +229,7 @@ Manager::Manager(QWidget *selectorParent, Fn buttonUpdate) .emoji = _list.front().emoji, }); } -}))) -, _selectorParent(selectorParent) { +}))) { _inner.translate(QRectF({}, _outer).center() - _inner.center()); _innerActive.translate( QRect({}, _outer).center() - _innerActive.center()); @@ -337,22 +258,16 @@ Manager::Manager(QWidget *selectorParent, Fn buttonUpdate) Manager::~Manager() = default; -void Manager::showButton(ButtonParameters parameters) { +void Manager::updateButton(ButtonParameters parameters) { if (_button && _buttonContext != parameters.context) { - if (!parameters.context - && _selector - && _selectorContext == _buttonContext) { - return; - } _button->applyState(ButtonState::Hidden); _buttonHiding.push_back(std::move(_button)); } _buttonContext = parameters.context; - if (!_buttonContext || _list.size() < 2) { - hideSelectors(anim::type::normal); + parameters.reactionsCount = _list.size(); + if (!_buttonContext || _list.empty()) { return; - } - if (!_button) { + } else if (!_button) { _button = std::make_unique